// 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. import 'dart:math' as math; import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'button_style.dart'; import 'color_scheme.dart'; import 'constants.dart'; import 'ink_ripple.dart'; import 'material_state.dart'; import 'text_button.dart'; import 'theme.dart'; import 'theme_data.dart'; import 'toggle_buttons_theme.dart'; // Examples can assume: // List<bool> isSelected = <bool>[]; // void setState(dynamic arg) { } /// A set of toggle buttons. /// /// The list of [children] are laid out along [direction]. The state of each button /// is controlled by [isSelected], which is a list of bools that determine /// if a button is in an unselected or selected state. They are both /// correlated by their index in the list. The length of [isSelected] has to /// match the length of the [children] list. /// /// {@youtube 560 315 https://www.youtube.com/watch?v=kVEguaQWGAY} /// /// {@tool dartpad} /// This example showcase [ToggleButtons] in various configurations. /// /// ** See code in examples/api/lib/material/toggle_buttons/toggle_buttons.0.dart ** /// {@end-tool} /// /// ## Customizing toggle buttons /// Each toggle's behavior can be configured by the [onPressed] callback, which /// can update the [isSelected] list however it wants to. /// /// {@animation 700 150 https://flutter.github.io/assets-for-api-docs/assets/material/toggle_buttons_simple.mp4} /// /// Here is an implementation that allows for multiple buttons to be /// simultaneously selected, while requiring none of the buttons to be /// selected. /// /// ```dart /// ToggleButtons( /// isSelected: isSelected, /// onPressed: (int index) { /// setState(() { /// isSelected[index] = !isSelected[index]; /// }); /// }, /// children: const <Widget>[ /// Icon(Icons.ac_unit), /// Icon(Icons.call), /// Icon(Icons.cake), /// ], /// ), /// ``` /// /// {@animation 700 150 https://flutter.github.io/assets-for-api-docs/assets/material/toggle_buttons_required_mutually_exclusive.mp4} /// /// Here is an implementation that requires mutually exclusive selection while /// requiring at least one selection. This assumes that [isSelected] was /// properly initialized with one selection. /// /// ```dart /// ToggleButtons( /// isSelected: isSelected, /// onPressed: (int index) { /// setState(() { /// for (int buttonIndex = 0; buttonIndex < isSelected.length; buttonIndex++) { /// if (buttonIndex == index) { /// isSelected[buttonIndex] = true; /// } else { /// isSelected[buttonIndex] = false; /// } /// } /// }); /// }, /// children: const <Widget>[ /// Icon(Icons.ac_unit), /// Icon(Icons.call), /// Icon(Icons.cake), /// ], /// ), /// ``` /// /// {@animation 700 150 https://flutter.github.io/assets-for-api-docs/assets/material/toggle_buttons_mutually_exclusive.mp4} /// /// Here is an implementation that requires mutually exclusive selection, /// but allows for none of the buttons to be selected. /// /// ```dart /// ToggleButtons( /// isSelected: isSelected, /// onPressed: (int index) { /// setState(() { /// for (int buttonIndex = 0; buttonIndex < isSelected.length; buttonIndex++) { /// if (buttonIndex == index) { /// isSelected[buttonIndex] = !isSelected[buttonIndex]; /// } else { /// isSelected[buttonIndex] = false; /// } /// } /// }); /// }, /// children: const <Widget>[ /// Icon(Icons.ac_unit), /// Icon(Icons.call), /// Icon(Icons.cake), /// ], /// ), /// ``` /// /// {@animation 700 150 https://flutter.github.io/assets-for-api-docs/assets/material/toggle_buttons_required.mp4} /// /// Here is an implementation that allows for multiple buttons to be /// simultaneously selected, while requiring at least one selection. This /// assumes that [isSelected] was properly initialized with one selection. /// /// ```dart /// ToggleButtons( /// isSelected: isSelected, /// onPressed: (int index) { /// int count = 0; /// for (final bool value in isSelected) { /// if (value) { /// count += 1; /// } /// } /// if (isSelected[index] && count < 2) { /// return; /// } /// setState(() { /// isSelected[index] = !isSelected[index]; /// }); /// }, /// children: const <Widget>[ /// Icon(Icons.ac_unit), /// Icon(Icons.call), /// Icon(Icons.cake), /// ], /// ), /// ``` /// /// ## ToggleButton Borders /// The toggle buttons, by default, have a solid, 1 logical pixel border /// surrounding itself and separating each button. The toggle button borders' /// color, width, and corner radii are configurable. /// /// The [selectedBorderColor] determines the border's color when the button is /// selected, while [disabledBorderColor] determines the border's color when /// the button is disabled. [borderColor] is used when the button is enabled. /// /// To remove the border, set [renderBorder] to false. Setting [borderWidth] to /// 0.0 results in a hairline border. For more information on hairline borders, /// see [BorderSide.width]. /// /// See also: /// /// * <https://material.io/design/components/buttons.html#toggle-button> class ToggleButtons extends StatelessWidget { /// Creates a set of toggle buttons. /// /// It displays its widgets provided in a [List] of [children] along [direction]. /// The state of each button is controlled by [isSelected], which is a list /// of bools that determine if a button is in an active, disabled, or /// selected state. They are both correlated by their index in the list. /// The length of [isSelected] has to match the length of the [children] /// list. /// /// Both [children] and [isSelected] properties arguments are required. /// /// [isSelected] values must be non-null. [focusNodes] must be null or a /// list of non-null nodes. [renderBorder] and [direction] must not be null. /// If [direction] is [Axis.vertical], [verticalDirection] must not be null. const ToggleButtons({ super.key, required this.children, required this.isSelected, this.onPressed, this.mouseCursor, this.tapTargetSize, this.textStyle, this.constraints, this.color, this.selectedColor, this.disabledColor, this.fillColor, this.focusColor, this.highlightColor, this.hoverColor, this.splashColor, this.focusNodes, this.renderBorder = true, this.borderColor, this.selectedBorderColor, this.disabledBorderColor, this.borderRadius, this.borderWidth, this.direction = Axis.horizontal, this.verticalDirection = VerticalDirection.down, }) : assert(children != null), assert(isSelected != null), assert(children.length == isSelected.length), assert(direction != null), assert(direction == Axis.horizontal || verticalDirection != null); static const double _defaultBorderWidth = 1.0; /// The toggle button widgets. /// /// These are typically [Icon] or [Text] widgets. The boolean selection /// state of each widget is defined by the corresponding [isSelected] /// list item. /// /// The length of children has to match the length of [isSelected]. If /// [focusNodes] is not null, the length of children has to also match /// the length of [focusNodes]. final List<Widget> children; /// The corresponding selection state of each toggle button. /// /// Each value in this list represents the selection state of the [children] /// widget at the same index. /// /// The length of [isSelected] has to match the length of [children]. final List<bool> isSelected; /// The callback that is called when a button is tapped. /// /// The index parameter of the callback is the index of the button that is /// tapped or otherwise activated. /// /// When the callback is null, all toggle buttons will be disabled. final void Function(int index)? onPressed; /// {@macro flutter.material.RawMaterialButton.mouseCursor} /// /// If this property is null, [MaterialStateMouseCursor.clickable] will be used. final MouseCursor? mouseCursor; /// Configures the minimum size of the area within which the buttons may /// be pressed. /// /// If the [tapTargetSize] is larger than [constraints], the buttons will /// include a transparent margin that responds to taps. /// /// Defaults to [ThemeData.materialTapTargetSize]. final MaterialTapTargetSize? tapTargetSize; /// The [TextStyle] to apply to any text in these toggle buttons. /// /// [TextStyle.color] will be ignored and substituted by [color], /// [selectedColor] or [disabledColor] depending on whether the buttons /// are active, selected, or disabled. final TextStyle? textStyle; /// Defines the button's size. /// /// Typically used to constrain the button's minimum size. /// /// If this property is null, then /// BoxConstraints(minWidth: 48.0, minHeight: 48.0) is be used. final BoxConstraints? constraints; /// The color for descendant [Text] and [Icon] widgets if the button is /// enabled and not selected. /// /// If [onPressed] is not null, this color will be used for values in /// [isSelected] that are false. /// /// If this property is null, then ToggleButtonTheme.of(context).color /// is used. If [ToggleButtonsThemeData.color] is also null, then /// Theme.of(context).colorScheme.onSurface is used. final Color? color; /// The color for descendant [Text] and [Icon] widgets if the button is /// selected. /// /// If [onPressed] is not null, this color will be used for values in /// [isSelected] that are true. /// /// If this property is null, then /// ToggleButtonTheme.of(context).selectedColor is used. If /// [ToggleButtonsThemeData.selectedColor] is also null, then /// Theme.of(context).colorScheme.primary is used. final Color? selectedColor; /// The color for descendant [Text] and [Icon] widgets if the button is /// disabled. /// /// If [onPressed] is null, this color will be used. /// /// If this property is null, then /// ToggleButtonTheme.of(context).disabledColor is used. If /// [ToggleButtonsThemeData.disabledColor] is also null, then /// Theme.of(context).colorScheme.onSurface.withOpacity(0.38) is used. final Color? disabledColor; /// The fill color for selected toggle buttons. /// /// If this property is null, then /// ToggleButtonTheme.of(context).fillColor is used. If /// [ToggleButtonsThemeData.fillColor] is also null, then /// the fill color is null. /// /// If fillColor is a [MaterialStateProperty<Color>], then [MaterialStateProperty.resolve] /// is used for the following [MaterialState]s: /// /// * [MaterialState.disabled] /// * [MaterialState.selected] /// final Color? fillColor; /// The color to use for filling the button when the button has input focus. /// /// If this property is null, then /// ToggleButtonTheme.of(context).focusColor is used. If /// [ToggleButtonsThemeData.focusColor] is also null, then /// Theme.of(context).focusColor is used. final Color? focusColor; /// The highlight color for the button's [InkWell]. /// /// If this property is null, then /// ToggleButtonTheme.of(context).highlightColor is used. If /// [ToggleButtonsThemeData.highlightColor] is also null, then /// Theme.of(context).highlightColor is used. final Color? highlightColor; /// The splash color for the button's [InkWell]. /// /// If this property is null, then /// ToggleButtonTheme.of(context).splashColor is used. If /// [ToggleButtonsThemeData.splashColor] is also null, then /// Theme.of(context).splashColor is used. final Color? splashColor; /// The color to use for filling the button when the button has a pointer /// hovering over it. /// /// If this property is null, then /// ToggleButtonTheme.of(context).hoverColor is used. If /// [ToggleButtonsThemeData.hoverColor] is also null, then /// Theme.of(context).hoverColor is used. final Color? hoverColor; /// The list of [FocusNode]s, corresponding to each toggle button. /// /// Focus is used to determine which widget should be affected by keyboard /// events. The focus tree keeps track of which widget is currently focused /// on by the user. /// /// If not null, the length of focusNodes has to match the length of /// [children]. /// /// See [FocusNode] for more information about how focus nodes are used. final List<FocusNode>? focusNodes; /// Whether or not to render a border around each toggle button. /// /// When true, a border with [borderWidth], [borderRadius] and the /// appropriate border color will render. Otherwise, no border will be /// rendered. final bool renderBorder; /// The border color to display when the toggle button is enabled and not /// selected. /// /// If this property is null, then /// ToggleButtonTheme.of(context).borderColor is used. If /// [ToggleButtonsThemeData.borderColor] is also null, then /// Theme.of(context).colorScheme.onSurface is used. final Color? borderColor; /// The border color to display when the toggle button is selected. /// /// If this property is null, then /// ToggleButtonTheme.of(context).selectedBorderColor is used. If /// [ToggleButtonsThemeData.selectedBorderColor] is also null, then /// Theme.of(context).colorScheme.primary is used. final Color? selectedBorderColor; /// The border color to display when the toggle button is disabled. /// /// If this property is null, then /// ToggleButtonTheme.of(context).disabledBorderColor is used. If /// [ToggleButtonsThemeData.disabledBorderColor] is also null, then /// Theme.of(context).disabledBorderColor is used. final Color? disabledBorderColor; /// The width of the border surrounding each toggle button. /// /// This applies to both the greater surrounding border, as well as the /// borders rendered between toggle buttons. /// /// To render a hairline border (one physical pixel), set borderWidth to 0.0. /// See [BorderSide.width] for more details on hairline borders. /// /// To omit the border entirely, set [renderBorder] to false. /// /// If this property is null, then /// ToggleButtonTheme.of(context).borderWidth is used. If /// [ToggleButtonsThemeData.borderWidth] is also null, then /// a width of 1.0 is used. final double? borderWidth; /// The radii of the border's corners. /// /// If this property is null, then /// ToggleButtonTheme.of(context).borderRadius is used. If /// [ToggleButtonsThemeData.borderRadius] is also null, then /// the buttons default to non-rounded borders. final BorderRadius? borderRadius; /// The direction along which the buttons are rendered. /// /// Defaults to [Axis.horizontal]. final Axis direction; /// If [direction] is [Axis.vertical], this parameter determines whether to lay out /// the buttons starting from the first or last child from top to bottom. final VerticalDirection verticalDirection; // Determines if this is the first child that is being laid out // by the render object, _not_ the order of the children in its list. bool _isFirstButton(int index, int length, TextDirection textDirection) { return index == 0 && ((direction == Axis.horizontal && textDirection == TextDirection.ltr) || (direction == Axis.vertical && verticalDirection == VerticalDirection.down)) || index == length - 1 && ((direction == Axis.horizontal && textDirection == TextDirection.rtl) || (direction == Axis.vertical && verticalDirection == VerticalDirection.up)); } // Determines if this is the last child that is being laid out // by the render object, _not_ the order of the children in its list. bool _isLastButton(int index, int length, TextDirection textDirection) { return index == length - 1 && ((direction == Axis.horizontal && textDirection == TextDirection.ltr) || (direction == Axis.vertical && verticalDirection == VerticalDirection.down)) || index == 0 && ((direction == Axis.horizontal && textDirection == TextDirection.rtl) || (direction == Axis.vertical && verticalDirection == VerticalDirection.up)); } BorderRadius _getEdgeBorderRadius( int index, int length, TextDirection textDirection, ToggleButtonsThemeData toggleButtonsTheme, ) { final BorderRadius resultingBorderRadius = borderRadius ?? toggleButtonsTheme.borderRadius ?? BorderRadius.zero; if (length == 1) { return resultingBorderRadius; } else if (direction == Axis.horizontal) { if (_isFirstButton(index, length, textDirection)) { return BorderRadius.only( topLeft: resultingBorderRadius.topLeft, bottomLeft: resultingBorderRadius.bottomLeft, ); } else if (_isLastButton(index, length, textDirection)) { return BorderRadius.only( topRight: resultingBorderRadius.topRight, bottomRight: resultingBorderRadius.bottomRight, ); } } else { if (_isFirstButton(index, length, textDirection)) { return BorderRadius.only( topLeft: resultingBorderRadius.topLeft, topRight: resultingBorderRadius.topRight, ); } else if (_isLastButton(index, length, textDirection)) { return BorderRadius.only( bottomLeft: resultingBorderRadius.bottomLeft, bottomRight: resultingBorderRadius.bottomRight, ); } } return BorderRadius.zero; } BorderRadius _getClipBorderRadius( int index, int length, TextDirection textDirection, ToggleButtonsThemeData toggleButtonsTheme, ) { final BorderRadius resultingBorderRadius = borderRadius ?? toggleButtonsTheme.borderRadius ?? BorderRadius.zero; final double resultingBorderWidth = borderWidth ?? toggleButtonsTheme.borderWidth ?? _defaultBorderWidth; if (length == 1) { return BorderRadius.only( topLeft: resultingBorderRadius.topLeft - Radius.circular(resultingBorderWidth / 2.0), bottomLeft: resultingBorderRadius.bottomLeft - Radius.circular(resultingBorderWidth / 2.0), topRight: resultingBorderRadius.topRight - Radius.circular(resultingBorderWidth / 2.0), bottomRight: resultingBorderRadius.bottomRight - Radius.circular(resultingBorderWidth / 2.0), ); } else if (direction == Axis.horizontal) { if (_isFirstButton(index, length, textDirection)) { return BorderRadius.only( topLeft: resultingBorderRadius.topLeft - Radius.circular(resultingBorderWidth / 2.0), bottomLeft: resultingBorderRadius.bottomLeft - Radius.circular(resultingBorderWidth / 2.0), ); } else if (_isLastButton(index, length, textDirection)) { return BorderRadius.only( topRight: resultingBorderRadius.topRight - Radius.circular(resultingBorderWidth / 2.0), bottomRight: resultingBorderRadius.bottomRight - Radius.circular(resultingBorderWidth / 2.0), ); } } else { if (_isFirstButton(index, length, textDirection)) { return BorderRadius.only( topLeft: resultingBorderRadius.topLeft - Radius.circular(resultingBorderWidth / 2.0), topRight: resultingBorderRadius.topRight - Radius.circular(resultingBorderWidth / 2.0), ); } else if (_isLastButton(index, length, textDirection)) { return BorderRadius.only( bottomLeft: resultingBorderRadius.bottomLeft - Radius.circular(resultingBorderWidth / 2.0), bottomRight: resultingBorderRadius.bottomRight - Radius.circular(resultingBorderWidth / 2.0), ); } } return BorderRadius.zero; } BorderSide _getLeadingBorderSide( int index, ThemeData theme, ToggleButtonsThemeData toggleButtonsTheme, ) { if (!renderBorder) { return BorderSide.none; } final double resultingBorderWidth = borderWidth ?? toggleButtonsTheme.borderWidth ?? _defaultBorderWidth; if (onPressed != null && (isSelected[index] || (index != 0 && isSelected[index - 1]))) { return BorderSide( color: selectedBorderColor ?? toggleButtonsTheme.selectedBorderColor ?? theme.colorScheme.onSurface.withOpacity(0.12), width: resultingBorderWidth, ); } else if (onPressed != null && !isSelected[index]) { return BorderSide( color: borderColor ?? toggleButtonsTheme.borderColor ?? theme.colorScheme.onSurface.withOpacity(0.12), width: resultingBorderWidth, ); } else { return BorderSide( color: disabledBorderColor ?? toggleButtonsTheme.disabledBorderColor ?? theme.colorScheme.onSurface.withOpacity(0.12), width: resultingBorderWidth, ); } } BorderSide _getBorderSide( int index, ThemeData theme, ToggleButtonsThemeData toggleButtonsTheme, ) { if (!renderBorder) { return BorderSide.none; } final double resultingBorderWidth = borderWidth ?? toggleButtonsTheme.borderWidth ?? _defaultBorderWidth; if (onPressed != null && isSelected[index]) { return BorderSide( color: selectedBorderColor ?? toggleButtonsTheme.selectedBorderColor ?? theme.colorScheme.onSurface.withOpacity(0.12), width: resultingBorderWidth, ); } else if (onPressed != null && !isSelected[index]) { return BorderSide( color: borderColor ?? toggleButtonsTheme.borderColor ?? theme.colorScheme.onSurface.withOpacity(0.12), width: resultingBorderWidth, ); } else { return BorderSide( color: disabledBorderColor ?? toggleButtonsTheme.disabledBorderColor ?? theme.colorScheme.onSurface.withOpacity(0.12), width: resultingBorderWidth, ); } } BorderSide _getTrailingBorderSide( int index, ThemeData theme, ToggleButtonsThemeData toggleButtonsTheme, ) { if (!renderBorder) { return BorderSide.none; } if (index != children.length - 1) { return BorderSide.none; } final double resultingBorderWidth = borderWidth ?? toggleButtonsTheme.borderWidth ?? _defaultBorderWidth; if (onPressed != null && (isSelected[index])) { return BorderSide( color: selectedBorderColor ?? toggleButtonsTheme.selectedBorderColor ?? theme.colorScheme.onSurface.withOpacity(0.12), width: resultingBorderWidth, ); } else if (onPressed != null && !isSelected[index]) { return BorderSide( color: borderColor ?? toggleButtonsTheme.borderColor ?? theme.colorScheme.onSurface.withOpacity(0.12), width: resultingBorderWidth, ); } else { return BorderSide( color: disabledBorderColor ?? toggleButtonsTheme.disabledBorderColor ?? theme.colorScheme.onSurface.withOpacity(0.12), width: resultingBorderWidth, ); } } @override Widget build(BuildContext context) { assert( !isSelected.any((bool val) => val == null), 'All elements of isSelected must be non-null.\n' 'The current list of isSelected values is as follows:\n' '$isSelected', ); assert( focusNodes == null || !focusNodes!.any((FocusNode val) => val == null), 'All elements of focusNodes must be non-null.\n' 'The current list of focus node values is as follows:\n' '$focusNodes', ); assert( () { if (focusNodes != null) { return focusNodes!.length == children.length; } return true; }(), 'focusNodes.length must match children.length.\n' 'There are ${focusNodes!.length} focus nodes, while ' 'there are ${children.length} children.', ); final ThemeData theme = Theme.of(context); final ToggleButtonsThemeData toggleButtonsTheme = ToggleButtonsTheme.of(context); final TextDirection textDirection = Directionality.of(context); final List<Widget> buttons = List<Widget>.generate(children.length, (int index) { final BorderRadius edgeBorderRadius = _getEdgeBorderRadius(index, children.length, textDirection, toggleButtonsTheme); final BorderRadius clipBorderRadius = _getClipBorderRadius(index, children.length, textDirection, toggleButtonsTheme); final BorderSide leadingBorderSide = _getLeadingBorderSide(index, theme, toggleButtonsTheme); final BorderSide borderSide = _getBorderSide(index, theme, toggleButtonsTheme); final BorderSide trailingBorderSide = _getTrailingBorderSide(index, theme, toggleButtonsTheme); final Set<MaterialState> states = <MaterialState>{ if (isSelected[index] && onPressed != null) MaterialState.selected, if (onPressed == null) MaterialState.disabled, }; final Color effectiveFillColor = _ResolveFillColor(fillColor ?? toggleButtonsTheme.fillColor).resolve(states) ?? _DefaultFillColor(theme.colorScheme).resolve(states); final Color currentColor; if (onPressed != null && isSelected[index]) { currentColor = selectedColor ?? toggleButtonsTheme.selectedColor ?? theme.colorScheme.primary; } else if (onPressed != null && !isSelected[index]) { currentColor = color ?? toggleButtonsTheme.color ?? theme.colorScheme.onSurface.withOpacity(0.87); } else { currentColor = disabledColor ?? toggleButtonsTheme.disabledColor ?? theme.colorScheme.onSurface.withOpacity(0.38); } final TextStyle currentTextStyle = textStyle ?? toggleButtonsTheme.textStyle ?? theme.textTheme.bodyMedium!; final BoxConstraints? currentConstraints = constraints ?? toggleButtonsTheme.constraints; final Size minimumSize = currentConstraints == null ? const Size.square(kMinInteractiveDimension) : Size(currentConstraints.minWidth, currentConstraints.minHeight); final Size? maximumSize = currentConstraints == null ? null : Size(currentConstraints.maxWidth, currentConstraints.maxHeight); final Size minPaddingSize; switch (tapTargetSize ?? theme.materialTapTargetSize) { case MaterialTapTargetSize.padded: if (direction == Axis.horizontal) { minPaddingSize = const Size( 0.0, kMinInteractiveDimension, ); } else { minPaddingSize = const Size( kMinInteractiveDimension, 0.0, ); } assert(minPaddingSize.width >= 0.0); assert(minPaddingSize.height >= 0.0); break; case MaterialTapTargetSize.shrinkWrap: minPaddingSize = Size.zero; break; } Widget button = _SelectToggleButton( leadingBorderSide: leadingBorderSide, borderSide: borderSide, trailingBorderSide: trailingBorderSide, borderRadius: edgeBorderRadius, isFirstButton: index == 0, isLastButton: index == children.length - 1, direction: direction, verticalDirection: verticalDirection, child: ClipRRect( borderRadius: clipBorderRadius, child: TextButton( focusNode: focusNodes != null ? focusNodes![index] : null, style: ButtonStyle( backgroundColor: MaterialStatePropertyAll<Color?>(effectiveFillColor), foregroundColor: MaterialStatePropertyAll<Color?>(currentColor), overlayColor: _ToggleButtonDefaultOverlay( selected: onPressed != null && isSelected[index], unselected: onPressed != null && !isSelected[index], colorScheme: theme.colorScheme, disabledColor: disabledColor ?? toggleButtonsTheme.disabledColor, focusColor: focusColor ?? toggleButtonsTheme.focusColor, highlightColor: highlightColor ?? toggleButtonsTheme.highlightColor, hoverColor: hoverColor ?? toggleButtonsTheme.hoverColor, splashColor: splashColor ?? toggleButtonsTheme.splashColor, ), elevation: const MaterialStatePropertyAll<double>(0), textStyle: MaterialStatePropertyAll<TextStyle?>(currentTextStyle.copyWith( color: currentColor, )), padding: const MaterialStatePropertyAll<EdgeInsetsGeometry>(EdgeInsets.zero), minimumSize: MaterialStatePropertyAll<Size?>(minimumSize), maximumSize: MaterialStatePropertyAll<Size?>(maximumSize), shape: const MaterialStatePropertyAll<OutlinedBorder>(RoundedRectangleBorder()), mouseCursor: MaterialStatePropertyAll<MouseCursor?>(mouseCursor), visualDensity: VisualDensity.standard, tapTargetSize: MaterialTapTargetSize.shrinkWrap, animationDuration: kThemeChangeDuration, enableFeedback: true, alignment: Alignment.center, splashFactory: InkRipple.splashFactory, ), onPressed: onPressed != null ? () {onPressed!(index);} : null, child: children[index], ), ), ); if (currentConstraints != null) { button = Center(child: button); } return MergeSemantics( child: Semantics( container: true, checked: isSelected[index], enabled: onPressed != null, child: _InputPadding( minSize: minPaddingSize, direction: direction, child: button, ), ), ); }); if (direction == Axis.vertical) { return IntrinsicWidth( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, verticalDirection: verticalDirection, children: buttons, ), ); } return IntrinsicHeight( child: Row( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: buttons, ), ); } @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.add(FlagProperty('disabled', value: onPressed == null, ifTrue: 'Buttons are disabled', ifFalse: 'Buttons are enabled', )); textStyle?.debugFillProperties(properties, prefix: 'textStyle.'); properties.add(ColorProperty('color', color, defaultValue: null)); properties.add(ColorProperty('selectedColor', selectedColor, defaultValue: null)); properties.add(ColorProperty('disabledColor', disabledColor, defaultValue: null)); properties.add(ColorProperty('fillColor', fillColor, defaultValue: null)); properties.add(ColorProperty('focusColor', focusColor, defaultValue: null)); properties.add(ColorProperty('highlightColor', highlightColor, defaultValue: null)); properties.add(ColorProperty('hoverColor', hoverColor, defaultValue: null)); properties.add(ColorProperty('splashColor', splashColor, defaultValue: null)); properties.add(ColorProperty('borderColor', borderColor, defaultValue: null)); properties.add(ColorProperty('selectedBorderColor', selectedBorderColor, defaultValue: null)); properties.add(ColorProperty('disabledBorderColor', disabledBorderColor, defaultValue: null)); properties.add(DiagnosticsProperty<BorderRadius>('borderRadius', borderRadius, defaultValue: null)); properties.add(DoubleProperty('borderWidth', borderWidth, defaultValue: null)); properties.add(DiagnosticsProperty<Axis>('direction', direction, defaultValue: Axis.horizontal)); properties.add(DiagnosticsProperty<VerticalDirection>('verticalDirection', verticalDirection, defaultValue: VerticalDirection.down)); } } @immutable class _ResolveFillColor extends MaterialStateProperty<Color?> with Diagnosticable { _ResolveFillColor(this.primary); final Color? primary; @override Color? resolve(Set<MaterialState> states) { if (primary is MaterialStateProperty<Color>) { return MaterialStateProperty.resolveAs<Color?>(primary, states); } return states.contains(MaterialState.selected) ? primary : null; } } @immutable class _DefaultFillColor extends MaterialStateProperty<Color> with Diagnosticable { _DefaultFillColor(this.colorScheme); final ColorScheme colorScheme; @override Color resolve(Set<MaterialState> states) { if (states.contains(MaterialState.selected)) { return colorScheme.primary.withOpacity(0.12); } return colorScheme.surface.withOpacity(0.0); } } @immutable class _ToggleButtonDefaultOverlay extends MaterialStateProperty<Color?> { _ToggleButtonDefaultOverlay({ required this.selected, required this.unselected, this.colorScheme, this.focusColor, this.highlightColor, this.hoverColor, this.splashColor, this.disabledColor, }); final bool selected; final bool unselected; final ColorScheme? colorScheme; final Color? focusColor; final Color? highlightColor; final Color? hoverColor; final Color? splashColor; final Color? disabledColor; @override Color? resolve(Set<MaterialState> states) { if (selected) { if (states.contains(MaterialState.hovered)) { return hoverColor ?? colorScheme?.primary.withOpacity(0.04); } else if (states.contains(MaterialState.focused)) { return focusColor ?? colorScheme?.primary.withOpacity(0.12); } else if (states.contains(MaterialState.pressed)) { return splashColor ?? colorScheme?.primary.withOpacity(0.16); } } else if (unselected) { if (states.contains(MaterialState.hovered)) { return hoverColor ?? colorScheme?.onSurface.withOpacity(0.04); } else if (states.contains(MaterialState.focused)) { return focusColor ?? colorScheme?.onSurface.withOpacity(0.12); } else if (states.contains(MaterialState.pressed)) { return splashColor ?? highlightColor ?? colorScheme?.onSurface.withOpacity(0.16); } } return null; } @override String toString() { return ''' { selected: hovered: $hoverColor, otherwise: ${colorScheme?.primary.withOpacity(0.04)}, focused: $focusColor, otherwise: ${colorScheme?.primary.withOpacity(0.12)}, pressed: $splashColor, otherwise: ${colorScheme?.primary.withOpacity(0.16)}, unselected: hovered: $hoverColor, otherwise: ${colorScheme?.onSurface.withOpacity(0.04)}, focused: $focusColor, otherwise: ${colorScheme?.onSurface.withOpacity(0.12)}, pressed: $splashColor, otherwise: ${colorScheme?.onSurface.withOpacity(0.16)}, otherwise: null, } '''; } } class _SelectToggleButton extends SingleChildRenderObjectWidget { const _SelectToggleButton({ required Widget super.child, required this.leadingBorderSide, required this.borderSide, required this.trailingBorderSide, required this.borderRadius, required this.isFirstButton, required this.isLastButton, required this.direction, required this.verticalDirection, }); // The width and color of the button's leading side border. final BorderSide leadingBorderSide; // The width and color of the side borders. // // If [direction] is [Axis.horizontal], this corresponds to the width and color // of the button's top and bottom side borders. // // If [direction] is [Axis.vertical], this corresponds to the width and color // of the button's left and right side borders. final BorderSide borderSide; // The width and color of the button's trailing side border. final BorderSide trailingBorderSide; // The border radii of each corner of the button. final BorderRadius borderRadius; // Whether or not this toggle button is the first button in the list. final bool isFirstButton; // Whether or not this toggle button is the last button in the list. final bool isLastButton; // The direction along which the buttons are rendered. final Axis direction; // If [direction] is [Axis.vertical], this property defines whether or not this button in its list // of buttons is laid out starting from top to bottom or from bottom to top. final VerticalDirection verticalDirection; @override _SelectToggleButtonRenderObject createRenderObject(BuildContext context) => _SelectToggleButtonRenderObject( leadingBorderSide, borderSide, trailingBorderSide, borderRadius, isFirstButton, isLastButton, direction, verticalDirection, Directionality.of(context), ); @override void updateRenderObject(BuildContext context, _SelectToggleButtonRenderObject renderObject) { renderObject ..leadingBorderSide = leadingBorderSide ..borderSide = borderSide ..trailingBorderSide = trailingBorderSide ..borderRadius = borderRadius ..isFirstButton = isFirstButton ..isLastButton = isLastButton ..direction = direction ..verticalDirection = verticalDirection ..textDirection = Directionality.of(context); } } class _SelectToggleButtonRenderObject extends RenderShiftedBox { _SelectToggleButtonRenderObject( this._leadingBorderSide, this._borderSide, this._trailingBorderSide, this._borderRadius, this._isFirstButton, this._isLastButton, this._direction, this._verticalDirection, this._textDirection, [ RenderBox? child, ]) : super(child); Axis get direction => _direction; Axis _direction; set direction(Axis value) { if (_direction == value) { return; } _direction = value; markNeedsLayout(); } VerticalDirection get verticalDirection => _verticalDirection; VerticalDirection _verticalDirection; set verticalDirection(VerticalDirection value) { if (_verticalDirection == value) { return; } _verticalDirection = value; markNeedsLayout(); } // The width and color of the button's leading side border. BorderSide get leadingBorderSide => _leadingBorderSide; BorderSide _leadingBorderSide; set leadingBorderSide(BorderSide value) { if (_leadingBorderSide == value) { return; } _leadingBorderSide = value; markNeedsLayout(); } // The width and color of the button's top and bottom side borders. BorderSide get borderSide => _borderSide; BorderSide _borderSide; set borderSide(BorderSide value) { if (_borderSide == value) { return; } _borderSide = value; markNeedsLayout(); } // The width and color of the button's trailing side border. BorderSide get trailingBorderSide => _trailingBorderSide; BorderSide _trailingBorderSide; set trailingBorderSide(BorderSide value) { if (_trailingBorderSide == value) { return; } _trailingBorderSide = value; markNeedsLayout(); } // The border radii of each corner of the button. BorderRadius get borderRadius => _borderRadius; BorderRadius _borderRadius; set borderRadius(BorderRadius value) { if (_borderRadius == value) { return; } _borderRadius = value; markNeedsLayout(); } // Whether or not this toggle button is the first button in the list. bool get isFirstButton => _isFirstButton; bool _isFirstButton; set isFirstButton(bool value) { if (_isFirstButton == value) { return; } _isFirstButton = value; markNeedsLayout(); } // Whether or not this toggle button is the last button in the list. bool get isLastButton => _isLastButton; bool _isLastButton; set isLastButton(bool value) { if (_isLastButton == value) { return; } _isLastButton = value; markNeedsLayout(); } // The direction in which text flows for this application. TextDirection get textDirection => _textDirection; TextDirection _textDirection; set textDirection(TextDirection value) { if (_textDirection == value) { return; } _textDirection = value; markNeedsLayout(); } static double _maxHeight(RenderBox? box, double width) { return box == null ? 0.0 : box.getMaxIntrinsicHeight(width); } static double _minHeight(RenderBox? box, double width) { return box == null ? 0.0 : box.getMinIntrinsicHeight(width); } static double _minWidth(RenderBox? box, double height) { return box == null ? 0.0 : box.getMinIntrinsicWidth(height); } static double _maxWidth(RenderBox? box, double height) { return box == null ? 0.0 : box.getMaxIntrinsicWidth(height); } @override double computeDistanceToActualBaseline(TextBaseline baseline) { // The baseline of this widget is the baseline of its child return direction == Axis.horizontal ? child!.computeDistanceToActualBaseline(baseline)! + borderSide.width : child!.computeDistanceToActualBaseline(baseline)! + leadingBorderSide.width; } @override double computeMaxIntrinsicHeight(double width) { return direction == Axis.horizontal ? borderSide.width * 2.0 + _maxHeight(child, width) : leadingBorderSide.width + _maxHeight(child, width) + trailingBorderSide.width; } @override double computeMinIntrinsicHeight(double width) { return direction == Axis.horizontal ? borderSide.width * 2.0 + _minHeight(child, width) : leadingBorderSide.width + _maxHeight(child, width) + trailingBorderSide.width; } @override double computeMaxIntrinsicWidth(double height) { return direction == Axis.horizontal ? leadingBorderSide.width + _maxWidth(child, height) + trailingBorderSide.width : borderSide.width * 2.0 + _maxWidth(child, height); } @override double computeMinIntrinsicWidth(double height) { return direction == Axis.horizontal ? leadingBorderSide.width + _minWidth(child, height) + trailingBorderSide.width : borderSide.width * 2.0 + _minWidth(child, height); } @override Size computeDryLayout(BoxConstraints constraints) { return _computeSize( constraints: constraints, layoutChild: ChildLayoutHelper.dryLayoutChild, ); } @override void performLayout() { size = _computeSize( constraints: constraints, layoutChild: ChildLayoutHelper.layoutChild, ); if (child == null) { return; } final BoxParentData childParentData = child!.parentData! as BoxParentData; if (direction == Axis.horizontal) { switch (textDirection) { case TextDirection.ltr: childParentData.offset = Offset(leadingBorderSide.width, borderSide.width); break; case TextDirection.rtl: childParentData.offset = Offset(trailingBorderSide.width, borderSide.width); break; } } else { switch (verticalDirection) { case VerticalDirection.down: childParentData.offset = Offset(borderSide.width, leadingBorderSide.width); break; case VerticalDirection.up: childParentData.offset = Offset(borderSide.width, trailingBorderSide.width); break; } } } Size _computeSize({required BoxConstraints constraints, required ChildLayouter layoutChild}) { if (child == null) { if (direction == Axis.horizontal) { return constraints.constrain(Size( leadingBorderSide.width + trailingBorderSide.width, borderSide.width * 2.0, )); } else { return constraints.constrain(Size( borderSide.width * 2.0, leadingBorderSide.width + trailingBorderSide.width, )); } } final double leftConstraint; final double rightConstraint; final double topConstraint; final double bottomConstraint; // It does not matter what [textDirection] or [verticalDirection] is, // since deflating the size constraints horizontally/vertically // and the returned size accounts for the width of both sides. if (direction == Axis.horizontal) { rightConstraint = trailingBorderSide.width; leftConstraint = leadingBorderSide.width; topConstraint = borderSide.width; bottomConstraint = borderSide.width; } else { rightConstraint = borderSide.width; leftConstraint = borderSide.width; topConstraint = leadingBorderSide.width; bottomConstraint = trailingBorderSide.width; } final BoxConstraints innerConstraints = constraints.deflate( EdgeInsets.only( left: leftConstraint, top: topConstraint, right: rightConstraint, bottom: bottomConstraint, ), ); final Size childSize = layoutChild(child!, innerConstraints); return constraints.constrain(Size( leftConstraint + childSize.width + rightConstraint, topConstraint + childSize.height + bottomConstraint, )); } @override void paint(PaintingContext context, Offset offset) { super.paint(context, offset); final Offset bottomRight = size.bottomRight(offset); final Rect outer = Rect.fromLTRB(offset.dx, offset.dy, bottomRight.dx, bottomRight.dy); final Rect center = outer.deflate(borderSide.width / 2.0); const double sweepAngle = math.pi / 2.0; final RRect rrect = RRect.fromRectAndCorners( center, topLeft: (borderRadius.topLeft.x * borderRadius.topLeft.y != 0.0) ? borderRadius.topLeft : Radius.zero, topRight: (borderRadius.topRight.x * borderRadius.topRight.y != 0.0) ? borderRadius.topRight : Radius.zero, bottomLeft: (borderRadius.bottomLeft.x * borderRadius.bottomLeft.y != 0.0) ? borderRadius.bottomLeft : Radius.zero, bottomRight: (borderRadius.bottomRight.x * borderRadius.bottomRight.y != 0.0) ? borderRadius.bottomRight : Radius.zero, ).scaleRadii(); final Rect tlCorner = Rect.fromLTWH( rrect.left, rrect.top, rrect.tlRadiusX * 2.0, rrect.tlRadiusY * 2.0, ); final Rect blCorner = Rect.fromLTWH( rrect.left, rrect.bottom - (rrect.blRadiusY * 2.0), rrect.blRadiusX * 2.0, rrect.blRadiusY * 2.0, ); final Rect trCorner = Rect.fromLTWH( rrect.right - (rrect.trRadiusX * 2), rrect.top, rrect.trRadiusX * 2, rrect.trRadiusY * 2, ); final Rect brCorner = Rect.fromLTWH( rrect.right - (rrect.brRadiusX * 2), rrect.bottom - (rrect.brRadiusY * 2), rrect.brRadiusX * 2, rrect.brRadiusY * 2, ); final Paint leadingPaint = leadingBorderSide.toPaint(); // Only one button. if (isFirstButton && isLastButton) { final Path leadingPath = Path(); final double startX = (rrect.brRadiusX == 0.0) ? outer.right : rrect.right - rrect.brRadiusX; leadingPath..moveTo(startX, rrect.bottom) ..lineTo(rrect.left + rrect.blRadiusX, rrect.bottom) ..addArc(blCorner, math.pi / 2.0, sweepAngle) ..lineTo(rrect.left, rrect.top + rrect.tlRadiusY) ..addArc(tlCorner, math.pi, sweepAngle) ..lineTo(rrect.right - rrect.trRadiusX, rrect.top) ..addArc(trCorner, math.pi * 3.0 / 2.0, sweepAngle) ..lineTo(rrect.right, rrect.bottom - rrect.brRadiusY) ..addArc(brCorner, 0, sweepAngle); context.canvas.drawPath(leadingPath, leadingPaint); return; } if (direction == Axis.horizontal) { switch (textDirection) { case TextDirection.ltr: if (isLastButton) { final Path leftPath = Path(); leftPath..moveTo(rrect.left, rrect.bottom + leadingBorderSide.width / 2) ..lineTo(rrect.left, rrect.top - leadingBorderSide.width / 2); context.canvas.drawPath(leftPath, leadingPaint); final Paint endingPaint = trailingBorderSide.toPaint(); final Path endingPath = Path(); endingPath..moveTo(rrect.left + borderSide.width / 2.0, rrect.top) ..lineTo(rrect.right - rrect.trRadiusX, rrect.top) ..addArc(trCorner, math.pi * 3.0 / 2.0, sweepAngle) ..lineTo(rrect.right, rrect.bottom - rrect.brRadiusY) ..addArc(brCorner, 0, sweepAngle) ..lineTo(rrect.left + borderSide.width / 2.0, rrect.bottom); context.canvas.drawPath(endingPath, endingPaint); } else if (isFirstButton) { final Path leadingPath = Path(); leadingPath..moveTo(outer.right, rrect.bottom) ..lineTo(rrect.left + rrect.blRadiusX, rrect.bottom) ..addArc(blCorner, math.pi / 2.0, sweepAngle) ..lineTo(rrect.left, rrect.top + rrect.tlRadiusY) ..addArc(tlCorner, math.pi, sweepAngle) ..lineTo(outer.right, rrect.top); context.canvas.drawPath(leadingPath, leadingPaint); } else { final Path leadingPath = Path(); leadingPath..moveTo(rrect.left, rrect.bottom + leadingBorderSide.width / 2) ..lineTo(rrect.left, rrect.top - leadingBorderSide.width / 2); context.canvas.drawPath(leadingPath, leadingPaint); final Paint horizontalPaint = borderSide.toPaint(); final Path horizontalPaths = Path(); horizontalPaths..moveTo(rrect.left + borderSide.width / 2.0, rrect.top) ..lineTo(outer.right - rrect.trRadiusX, rrect.top) ..moveTo(rrect.left + borderSide.width / 2.0 + rrect.tlRadiusX, rrect.bottom) ..lineTo(outer.right - rrect.trRadiusX, rrect.bottom); context.canvas.drawPath(horizontalPaths, horizontalPaint); } break; case TextDirection.rtl: if (isLastButton) { final Path leadingPath = Path(); leadingPath..moveTo(rrect.right, rrect.bottom + leadingBorderSide.width / 2) ..lineTo(rrect.right, rrect.top - leadingBorderSide.width / 2); context.canvas.drawPath(leadingPath, leadingPaint); final Paint endingPaint = trailingBorderSide.toPaint(); final Path endingPath = Path(); endingPath..moveTo(rrect.right - borderSide.width / 2.0, rrect.top) ..lineTo(rrect.left + rrect.tlRadiusX, rrect.top) ..addArc(tlCorner, math.pi * 3.0 / 2.0, -sweepAngle) ..lineTo(rrect.left, rrect.bottom - rrect.blRadiusY) ..addArc(blCorner, math.pi, -sweepAngle) ..lineTo(rrect.right - borderSide.width / 2.0, rrect.bottom); context.canvas.drawPath(endingPath, endingPaint); } else if (isFirstButton) { final Path leadingPath = Path(); leadingPath..moveTo(outer.left, rrect.bottom) ..lineTo(rrect.right - rrect.brRadiusX, rrect.bottom) ..addArc(brCorner, math.pi / 2.0, -sweepAngle) ..lineTo(rrect.right, rrect.top + rrect.trRadiusY) ..addArc(trCorner, 0, -sweepAngle) ..lineTo(outer.left, rrect.top); context.canvas.drawPath(leadingPath, leadingPaint); } else { final Path leadingPath = Path(); leadingPath..moveTo(rrect.right, rrect.bottom + leadingBorderSide.width / 2) ..lineTo(rrect.right, rrect.top - leadingBorderSide.width / 2); context.canvas.drawPath(leadingPath, leadingPaint); final Paint horizontalPaint = borderSide.toPaint(); final Path horizontalPaths = Path(); horizontalPaths..moveTo(rrect.right - borderSide.width / 2.0, rrect.top) ..lineTo(outer.left - rrect.tlRadiusX, rrect.top) ..moveTo(rrect.right - borderSide.width / 2.0 + rrect.trRadiusX, rrect.bottom) ..lineTo(outer.left - rrect.tlRadiusX, rrect.bottom); context.canvas.drawPath(horizontalPaths, horizontalPaint); } break; } } else { switch (verticalDirection) { case VerticalDirection.down: if (isLastButton) { final Path topPath = Path(); topPath..moveTo(outer.left, outer.top + leadingBorderSide.width / 2) ..lineTo(outer.right, outer.top + leadingBorderSide.width / 2); context.canvas.drawPath(topPath, leadingPaint); final Paint endingPaint = trailingBorderSide.toPaint(); final Path endingPath = Path(); endingPath..moveTo(rrect.left, rrect.top + leadingBorderSide.width / 2.0) ..lineTo(rrect.left, rrect.bottom - rrect.blRadiusY) ..addArc(blCorner, math.pi * 3.0, -sweepAngle) ..lineTo(rrect.right - rrect.blRadiusX, rrect.bottom) ..addArc(brCorner, math.pi / 2.0, -sweepAngle) ..lineTo(rrect.right, rrect.top + leadingBorderSide.width / 2.0); context.canvas.drawPath(endingPath, endingPaint); } else if (isFirstButton) { final Path leadingPath = Path(); leadingPath..moveTo(rrect.left, outer.bottom) ..lineTo(rrect.left, rrect.top + rrect.tlRadiusX) ..addArc(tlCorner, math.pi, sweepAngle) ..lineTo(rrect.right - rrect.trRadiusX, rrect.top) ..addArc(trCorner, math.pi * 3.0 / 2.0, sweepAngle) ..lineTo(rrect.right, outer.bottom); context.canvas.drawPath(leadingPath, leadingPaint); } else { final Path topPath = Path(); topPath..moveTo(outer.left, outer.top + leadingBorderSide.width / 2) ..lineTo(outer.right, outer.top + leadingBorderSide.width / 2); context.canvas.drawPath(topPath, leadingPaint); final Paint paint = borderSide.toPaint(); final Path paths = Path(); // Left and right borders. paths..moveTo(rrect.left, outer.top + leadingBorderSide.width) ..lineTo(rrect.left, outer.bottom) ..moveTo(rrect.right, outer.top + leadingBorderSide.width) ..lineTo(rrect.right, outer.bottom); context.canvas.drawPath(paths, paint); } break; case VerticalDirection.up: if (isLastButton) { final Path bottomPath = Path(); bottomPath..moveTo(outer.left, outer.bottom - leadingBorderSide.width / 2.0) ..lineTo(outer.right, outer.bottom - leadingBorderSide.width / 2.0); context.canvas.drawPath(bottomPath, leadingPaint); final Paint endingPaint = trailingBorderSide.toPaint(); final Path endingPath = Path(); endingPath..moveTo(rrect.left, rrect.bottom - leadingBorderSide.width / 2.0) ..lineTo(rrect.left, rrect.top + rrect.tlRadiusY) ..addArc(tlCorner, math.pi, sweepAngle) ..lineTo(rrect.right - rrect.trRadiusX, rrect.top) ..addArc(trCorner, math.pi * 3.0 / 2.0, sweepAngle) ..lineTo(rrect.right, rrect.bottom - leadingBorderSide.width / 2.0); context.canvas.drawPath(endingPath, endingPaint); } else if (isFirstButton) { final Path leadingPath = Path(); leadingPath..moveTo(rrect.left, outer.top) ..lineTo(rrect.left, rrect.bottom - rrect.blRadiusY) ..addArc(blCorner, math.pi, -sweepAngle) ..lineTo(rrect.right - rrect.brRadiusX, rrect.bottom) ..addArc(brCorner, math.pi / 2.0, -sweepAngle) ..lineTo(rrect.right, outer.top); context.canvas.drawPath(leadingPath, leadingPaint); } else { final Path bottomPath = Path(); bottomPath..moveTo(outer.left, outer.bottom - leadingBorderSide.width / 2.0) ..lineTo(outer.right, outer.bottom - leadingBorderSide.width / 2.0); context.canvas.drawPath(bottomPath, leadingPaint); final Paint paint = borderSide.toPaint(); final Path paths = Path(); // Left and right borders. paths..moveTo(rrect.left, outer.top) ..lineTo(rrect.left, outer.bottom - leadingBorderSide.width) ..moveTo(rrect.right, outer.top) ..lineTo(rrect.right, outer.bottom - leadingBorderSide.width); context.canvas.drawPath(paths, paint); } break; } } } } /// A widget to pad the area around a [ToggleButtons]'s children. /// /// This widget is based on a similar one used in [ButtonStyleButton] but it /// only redirects taps along one axis to ensure the correct button is tapped /// within the [ToggleButtons]. /// /// This ensures that a widget takes up at least as much space as the minSize /// parameter to ensure adequate tap target size, while keeping the widget /// visually smaller to the user. class _InputPadding extends SingleChildRenderObjectWidget { const _InputPadding({ super.child, required this.minSize, required this.direction, }); final Size minSize; final Axis direction; @override RenderObject createRenderObject(BuildContext context) { return _RenderInputPadding(minSize, direction); } @override void updateRenderObject(BuildContext context, covariant _RenderInputPadding renderObject) { renderObject.minSize = minSize; renderObject.direction = direction; } } class _RenderInputPadding extends RenderShiftedBox { _RenderInputPadding(this._minSize, this._direction, [RenderBox? child]) : super(child); Size get minSize => _minSize; Size _minSize; set minSize(Size value) { if (_minSize == value) { return; } _minSize = value; markNeedsLayout(); } Axis get direction => _direction; Axis _direction; set direction(Axis value) { if (_direction == value) { return; } _direction = 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 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 }) { // The super.hitTest() method also checks hitTestChildren(). We don't // want that in this case because we've padded around the children per // tapTargetSize. if (!size.contains(position)) { return false; } // Only adjust one axis to ensure the correct button is tapped. Offset center; if (direction == Axis.horizontal) { center = Offset(position.dx, child!.size.height / 2); } else { center = Offset(child!.size.width / 2, position.dy); } return result.addWithRawTransform( transform: MatrixUtils.forceToPoint(center), position: center, hitTest: (BoxHitTestResult result, Offset position) { assert(position == center); return child!.hitTest(result, position: center); }, ); } }