diff --git a/example/lib/src/storybook/stories/checkbox.dart b/example/lib/src/storybook/stories/checkbox.dart index 1cd9f6f3..4569a720 100644 --- a/example/lib/src/storybook/stories/checkbox.dart +++ b/example/lib/src/storybook/stories/checkbox.dart @@ -30,14 +30,23 @@ class CheckboxStory extends Story { final activeColor = colorTable(context)[activeColorsKnob]; - final fillColorsKnob = context.knobs.options( - label: "fillColor", + final inactiveColorsKnob = context.knobs.options( + label: "inactiveColor", description: "MoonColors variants for when Checkbox is unchecked.", initial: 39, // transparent options: colorOptions, ); - final fillColor = colorTable(context)[fillColorsKnob]; + final inactiveColor = colorTable(context)[inactiveColorsKnob]; + + final borderColorsKnob = context.knobs.options( + label: "borderColor", + description: "MoonColors variants for Checkbox border.", + initial: 6, // trunks + options: colorOptions, + ); + + final borderColor = colorTable(context)[borderColorsKnob]; final isDisabled = context.knobs.boolean( label: "Disabled", @@ -46,7 +55,7 @@ class CheckboxStory extends Story { final isTristate = context.knobs.boolean( label: "tristate", - description: "Whether the checkbox uses tristate.", + description: "Whether the Checkbox uses tristate.", ); final setRtlModeKnob = context.knobs.boolean( @@ -66,9 +75,10 @@ class CheckboxStory extends Story { StatefulBuilder( builder: (context, setState) { return MoonCheckbox( - checkColor: checkColor, activeColor: activeColor, - fillColor: fillColor, + borderColor: borderColor, + checkColor: checkColor, + inactiveColor: inactiveColor, tristate: isTristate, value: value, onChanged: isDisabled ? null : (newValue) => setState(() => value = newValue), @@ -84,7 +94,7 @@ class CheckboxStory extends Story { context, checkColor: checkColor, activeColor: activeColor, - fillColor: fillColor, + inactiveColor: inactiveColor, tristate: isTristate, value: value, onChanged: isDisabled ? null : (newValue) => setState(() => value = newValue), diff --git a/example/lib/src/storybook/stories/radio.dart b/example/lib/src/storybook/stories/radio.dart new file mode 100644 index 00000000..4dc9b716 --- /dev/null +++ b/example/lib/src/storybook/stories/radio.dart @@ -0,0 +1,108 @@ +import 'package:example/src/storybook/common/options.dart'; +import 'package:example/src/storybook/common/widgets/text_divider.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:moon_design/moon_design.dart'; +import 'package:storybook_flutter/storybook_flutter.dart'; + +enum ChoiceCustom { first, second } + +enum ChoiceLabel { first, second } + +ChoiceCustom? valueCustom = ChoiceCustom.first; +ChoiceLabel? valueLabel = ChoiceLabel.first; + +class RadioStory extends Story { + RadioStory() + : super( + name: "Radio", + builder: (context) { + final activeColorsKnob = context.knobs.options( + label: "activeColor", + description: "MoonColors variants for when Radio is checked.", + initial: 0, // piccolo + options: colorOptions, + ); + + final activeColor = colorTable(context)[activeColorsKnob]; + + final inactiveColorsKnob = context.knobs.options( + label: "inactiveColor", + description: "MoonColors variants for when Radio is unchecked.", + initial: 6, // trunks + options: colorOptions, + ); + + final inactiveColor = colorTable(context)[inactiveColorsKnob]; + + final isToggleable = context.knobs.boolean( + label: "toggleable", + description: "Whether the selected Radio can be unselected.", + ); + + final isDisabled = context.knobs.boolean( + label: "Disabled", + description: "onChanged() is null.", + ); + + final setRtlModeKnob = context.knobs.boolean( + label: "RTL mode", + description: "Switch between LTR and RTL modes.", + ); + + return Directionality( + textDirection: setRtlModeKnob ? TextDirection.rtl : TextDirection.ltr, + child: Center( + child: StatefulBuilder( + builder: (context, setState) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: 64), + const TextDivider(text: "Customisable Radio buttons"), + const SizedBox(height: 32), + MoonRadio( + value: ChoiceCustom.first, + groupValue: valueCustom, + onChanged: isDisabled ? null : (ChoiceCustom? choice) => setState(() => valueCustom = choice), + activeColor: activeColor, + inactiveColor: inactiveColor, + toggleable: isToggleable, + ), + const SizedBox(height: 8), + MoonRadio( + value: ChoiceCustom.second, + groupValue: valueCustom, + onChanged: isDisabled ? null : (ChoiceCustom? choice) => setState(() => valueCustom = choice), + activeColor: activeColor, + inactiveColor: inactiveColor, + toggleable: isToggleable, + ), + const SizedBox(height: 40), + const TextDivider(text: "Radios with clickable text"), + const SizedBox(height: 32), + MoonRadio.withLabel( + context, + value: ChoiceLabel.first, + groupValue: valueLabel, + label: "With label #1", + onChanged: isDisabled ? null : (ChoiceLabel? choice) => setState(() => valueLabel = choice), + ), + const SizedBox(height: 8), + MoonRadio.withLabel( + context, + value: ChoiceLabel.second, + groupValue: valueLabel, + label: "With label #2", + onChanged: isDisabled ? null : (ChoiceLabel? choice) => setState(() => valueLabel = choice), + ), + const SizedBox(height: 64), + ], + ); + }, + ), + ), + ); + }, + ); +} diff --git a/example/lib/src/storybook/storybook.dart b/example/lib/src/storybook/storybook.dart index 9d73e534..d5fe8ce6 100644 --- a/example/lib/src/storybook/storybook.dart +++ b/example/lib/src/storybook/storybook.dart @@ -10,6 +10,7 @@ import 'package:example/src/storybook/stories/icons.dart'; import 'package:example/src/storybook/stories/linear_loader.dart'; import 'package:example/src/storybook/stories/linear_progress.dart'; import 'package:example/src/storybook/stories/popover.dart'; +import 'package:example/src/storybook/stories/radio.dart'; import 'package:example/src/storybook/stories/switch.dart'; import 'package:example/src/storybook/stories/tag.dart'; import 'package:example/src/storybook/stories/tooltip.dart'; @@ -78,6 +79,7 @@ class StorybookPage extends StatelessWidget { LinearLoaderStory(), LinearProgressStory(), PopoverStory(), + RadioStory(), SwitchStory(), TagStory(), TooltipStory(), diff --git a/lib/moon_design.dart b/lib/moon_design.dart index 9ef92c53..ebbb678a 100644 --- a/lib/moon_design.dart +++ b/lib/moon_design.dart @@ -14,6 +14,7 @@ export 'package:moon_design/src/theme/opacity.dart'; export 'package:moon_design/src/theme/popover/popover_theme.dart'; export 'package:moon_design/src/theme/progress/circular_progress/circular_progress_theme.dart'; export 'package:moon_design/src/theme/progress/linear_progress/linear_progress_theme.dart'; +export 'package:moon_design/src/theme/radio/radio_theme.dart'; export 'package:moon_design/src/theme/shadows.dart'; export 'package:moon_design/src/theme/sizes.dart'; export 'package:moon_design/src/theme/switch/switch_theme.dart'; @@ -49,6 +50,7 @@ export 'package:moon_design/src/widgets/loaders/linear_loader.dart'; export 'package:moon_design/src/widgets/popover/popover.dart'; export 'package:moon_design/src/widgets/progress/circular_progress.dart'; export 'package:moon_design/src/widgets/progress/linear_progress.dart'; +export 'package:moon_design/src/widgets/radio/radio.dart'; export 'package:moon_design/src/widgets/switch/switch.dart'; export 'package:moon_design/src/widgets/tag/tag.dart'; export 'package:moon_design/src/widgets/tooltip/tooltip.dart'; diff --git a/lib/src/theme/checkbox/checkbox_colors.dart b/lib/src/theme/checkbox/checkbox_colors.dart index ad209d1c..56aa7840 100644 --- a/lib/src/theme/checkbox/checkbox_colors.dart +++ b/lib/src/theme/checkbox/checkbox_colors.dart @@ -8,14 +8,14 @@ class MoonCheckboxColors extends ThemeExtension with Diagnos static final light = MoonCheckboxColors( activeColor: MoonColors.light.piccolo, checkColor: MoonColors.light.goten, - fillColor: Colors.transparent, + inactiveColor: Colors.transparent, borderColor: MoonColors.light.trunks, ); static final dark = MoonCheckboxColors( activeColor: MoonColors.dark.piccolo, checkColor: MoonColors.dark.goten, - fillColor: Colors.transparent, + inactiveColor: Colors.transparent, borderColor: MoonColors.dark.trunks, ); @@ -28,14 +28,14 @@ class MoonCheckboxColors extends ThemeExtension with Diagnos /// Checkbox icon color. final Color checkColor; - /// Checkbox fill (inactive) color. - final Color fillColor; + /// Checkbox inactive color. + final Color inactiveColor; const MoonCheckboxColors({ required this.activeColor, required this.borderColor, required this.checkColor, - required this.fillColor, + required this.inactiveColor, }); @override @@ -43,13 +43,13 @@ class MoonCheckboxColors extends ThemeExtension with Diagnos Color? activeColor, Color? borderColor, Color? checkColor, - Color? fillColor, + Color? inactiveColor, }) { return MoonCheckboxColors( activeColor: activeColor ?? this.activeColor, borderColor: borderColor ?? this.borderColor, checkColor: checkColor ?? this.checkColor, - fillColor: fillColor ?? this.fillColor, + inactiveColor: inactiveColor ?? this.inactiveColor, ); } @@ -61,7 +61,7 @@ class MoonCheckboxColors extends ThemeExtension with Diagnos activeColor: Color.lerp(activeColor, other.activeColor, t)!, borderColor: Color.lerp(borderColor, other.borderColor, t)!, checkColor: Color.lerp(checkColor, other.checkColor, t)!, - fillColor: Color.lerp(fillColor, other.fillColor, t)!, + inactiveColor: Color.lerp(inactiveColor, other.inactiveColor, t)!, ); } @@ -73,6 +73,6 @@ class MoonCheckboxColors extends ThemeExtension with Diagnos ..add(ColorProperty("activeColor", activeColor)) ..add(ColorProperty("borderColor", borderColor)) ..add(ColorProperty("checkColor", checkColor)) - ..add(ColorProperty("fillColor", fillColor)); + ..add(ColorProperty("inactiveColor", inactiveColor)); } } diff --git a/lib/src/theme/radio/radio_colors.dart b/lib/src/theme/radio/radio_colors.dart new file mode 100644 index 00000000..6581d493 --- /dev/null +++ b/lib/src/theme/radio/radio_colors.dart @@ -0,0 +1,58 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'package:moon_design/src/theme/colors.dart'; + +@immutable +class MoonRadioColors extends ThemeExtension with DiagnosticableTreeMixin { + static final light = MoonRadioColors( + activeColor: MoonColors.light.piccolo, + inactiveColor: MoonColors.light.trunks, + ); + + static final dark = MoonRadioColors( + activeColor: MoonColors.dark.piccolo, + inactiveColor: MoonColors.dark.trunks, + ); + + /// Radio active color. + final Color activeColor; + + /// Radio inactive color. + final Color inactiveColor; + + const MoonRadioColors({ + required this.activeColor, + required this.inactiveColor, + }); + + @override + MoonRadioColors copyWith({ + Color? activeColor, + Color? inactiveColor, + }) { + return MoonRadioColors( + activeColor: activeColor ?? this.activeColor, + inactiveColor: inactiveColor ?? this.inactiveColor, + ); + } + + @override + MoonRadioColors lerp(ThemeExtension? other, double t) { + if (other is! MoonRadioColors) return this; + + return MoonRadioColors( + activeColor: Color.lerp(activeColor, other.activeColor, t)!, + inactiveColor: Color.lerp(inactiveColor, other.inactiveColor, t)!, + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty("type", "MoonRadioColors")) + ..add(ColorProperty("activeColor", activeColor)) + ..add(ColorProperty("inactiveColor", inactiveColor)); + } +} diff --git a/lib/src/theme/radio/radio_theme.dart b/lib/src/theme/radio/radio_theme.dart new file mode 100644 index 00000000..4845ef60 --- /dev/null +++ b/lib/src/theme/radio/radio_theme.dart @@ -0,0 +1,48 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'package:moon_design/src/theme/radio/radio_colors.dart'; + +@immutable +class MoonRadioTheme extends ThemeExtension with DiagnosticableTreeMixin { + static final light = MoonRadioTheme( + colors: MoonRadioColors.light, + ); + + static final dark = MoonRadioTheme( + colors: MoonRadioColors.dark, + ); + + /// Radio colors. + final MoonRadioColors colors; + + const MoonRadioTheme({ + required this.colors, + }); + + @override + MoonRadioTheme copyWith({ + MoonRadioColors? colors, + }) { + return MoonRadioTheme( + colors: colors ?? this.colors, + ); + } + + @override + MoonRadioTheme lerp(ThemeExtension? other, double t) { + if (other is! MoonRadioTheme) return this; + + return MoonRadioTheme( + colors: colors.lerp(other.colors, t), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder diagnosticProperties) { + super.debugFillProperties(diagnosticProperties); + diagnosticProperties + ..add(DiagnosticsProperty("type", "MoonRadioTheme")) + ..add(DiagnosticsProperty("colors", colors)); + } +} diff --git a/lib/src/theme/theme.dart b/lib/src/theme/theme.dart index fecbf2c0..04811531 100644 --- a/lib/src/theme/theme.dart +++ b/lib/src/theme/theme.dart @@ -1,7 +1,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:moon_design/src/theme/accordion/accordion_theme.dart'; +import 'package:moon_design/src/theme/accordion/accordion_theme.dart'; import 'package:moon_design/src/theme/avatar/avatar_theme.dart'; import 'package:moon_design/src/theme/borders.dart'; import 'package:moon_design/src/theme/button/button_theme.dart'; @@ -15,6 +15,7 @@ import 'package:moon_design/src/theme/opacity.dart'; import 'package:moon_design/src/theme/popover/popover_theme.dart'; import 'package:moon_design/src/theme/progress/circular_progress/circular_progress_theme.dart'; import 'package:moon_design/src/theme/progress/linear_progress/linear_progress_theme.dart'; +import 'package:moon_design/src/theme/radio/radio_theme.dart'; import 'package:moon_design/src/theme/shadows.dart'; import 'package:moon_design/src/theme/sizes.dart'; import 'package:moon_design/src/theme/switch/switch_theme.dart'; @@ -39,6 +40,7 @@ class MoonTheme extends ThemeExtension with DiagnosticableTreeMixin { linearProgressTheme: MoonLinearProgressTheme.light, opacity: MoonOpacity.opacities, popoverTheme: MoonPopoverTheme.light, + radioTheme: MoonRadioTheme.light, shadows: MoonShadows.light, sizes: MoonSizes.sizes, switchTheme: MoonSwitchTheme.light, @@ -62,6 +64,7 @@ class MoonTheme extends ThemeExtension with DiagnosticableTreeMixin { linearProgressTheme: MoonLinearProgressTheme.dark, opacity: MoonOpacity.opacities, popoverTheme: MoonPopoverTheme.dark, + radioTheme: MoonRadioTheme.dark, shadows: MoonShadows.dark, sizes: MoonSizes.sizes, switchTheme: MoonSwitchTheme.dark, @@ -112,6 +115,9 @@ class MoonTheme extends ThemeExtension with DiagnosticableTreeMixin { /// Moon Design System MoonPopover widget theming. final MoonPopoverTheme popoverTheme; + /// Moon Design System Radio widget theming. + final MoonRadioTheme radioTheme; + /// Moon Design System shadows. final MoonShadows shadows; @@ -145,6 +151,7 @@ class MoonTheme extends ThemeExtension with DiagnosticableTreeMixin { required this.linearProgressTheme, required this.opacity, required this.popoverTheme, + required this.radioTheme, required this.shadows, required this.sizes, required this.switchTheme, @@ -169,6 +176,7 @@ class MoonTheme extends ThemeExtension with DiagnosticableTreeMixin { MoonLinearProgressTheme? linearProgressTheme, MoonOpacity? opacity, MoonPopoverTheme? popoverTheme, + MoonRadioTheme? radioTheme, MoonShadows? shadows, MoonSizes? sizes, MoonSwitchTheme? switchTheme, @@ -191,6 +199,7 @@ class MoonTheme extends ThemeExtension with DiagnosticableTreeMixin { linearProgressTheme: linearProgressTheme ?? this.linearProgressTheme, opacity: opacity ?? this.opacity, popoverTheme: popoverTheme ?? this.popoverTheme, + radioTheme: radioTheme ?? this.radioTheme, shadows: shadows ?? this.shadows, sizes: sizes ?? this.sizes, switchTheme: switchTheme ?? this.switchTheme, @@ -219,6 +228,7 @@ class MoonTheme extends ThemeExtension with DiagnosticableTreeMixin { linearProgressTheme: linearProgressTheme.lerp(other.linearProgressTheme, t), opacity: opacity.lerp(other.opacity, t), popoverTheme: popoverTheme.lerp(other.popoverTheme, t), + radioTheme: radioTheme.lerp(other.radioTheme, t), shadows: shadows.lerp(other.shadows, t), sizes: sizes.lerp(other.sizes, t), switchTheme: switchTheme.lerp(other.switchTheme, t), @@ -247,6 +257,7 @@ class MoonTheme extends ThemeExtension with DiagnosticableTreeMixin { ..add(DiagnosticsProperty("MoonLinearProgressTheme", linearProgressTheme)) ..add(DiagnosticsProperty("MoonOpacity", opacity)) ..add(DiagnosticsProperty("MoonPopoverTheme", popoverTheme)) + ..add(DiagnosticsProperty("MoonRadioTheme", radioTheme)) ..add(DiagnosticsProperty("MoonShadows", shadows)) ..add(DiagnosticsProperty("MoonSizes", sizes)) ..add(DiagnosticsProperty("MoonSwitchTheme", switchTheme)) @@ -258,7 +269,6 @@ class MoonTheme extends ThemeExtension with DiagnosticableTreeMixin { extension MoonThemeX on BuildContext { MoonTheme? get moonTheme => Theme.of(this).extension(); - MoonAccordionTheme? get moonAccordionTheme => moonTheme?.accordionTheme; MoonBorders? get moonBorders => moonTheme?.borders; MoonColors? get moonColors => moonTheme?.colors; MoonEffects? get moonEffects => moonTheme?.effects; diff --git a/lib/src/widgets/checkbox/checkbox.dart b/lib/src/widgets/checkbox/checkbox.dart index ea4deed0..e565603b 100644 --- a/lib/src/widgets/checkbox/checkbox.dart +++ b/lib/src/widgets/checkbox/checkbox.dart @@ -1,10 +1,13 @@ import 'package:figma_squircle/figma_squircle.dart'; import 'package:flutter/material.dart'; -import 'package:moon_design/moon_design.dart'; +import 'package:moon_design/src/theme/colors.dart'; import 'package:moon_design/src/theme/effects/focus_effects.dart'; +import 'package:moon_design/src/theme/opacity.dart'; +import 'package:moon_design/src/theme/theme.dart'; import 'package:moon_design/src/utils/touch_target_padding.dart'; import 'package:moon_design/src/widgets/checkbox/checkbox_painter.dart'; +import 'package:moon_design/src/widgets/common/effects/focus_effect.dart'; class MoonCheckbox extends StatefulWidget { /// Whether this checkbox is checked. @@ -43,7 +46,7 @@ class MoonCheckbox extends StatefulWidget { final Color? checkColor; /// The color to use for the checkbox's background when the checkbox is not checked. - final Color? fillColor; + final Color? inactiveColor; /// {@macro flutter.widgets.Focus.focusNode}. final FocusNode? focusNode; @@ -51,6 +54,7 @@ class MoonCheckbox extends StatefulWidget { /// {@macro flutter.widgets.Focus.autofocus} final bool autofocus; + /// MDS checkbox widget. const MoonCheckbox({ super.key, required this.value, @@ -60,7 +64,7 @@ class MoonCheckbox extends StatefulWidget { this.activeColor, this.borderColor, this.checkColor, - this.fillColor, + this.inactiveColor, this.focusNode, this.autofocus = false, }); @@ -69,13 +73,13 @@ class MoonCheckbox extends StatefulWidget { BuildContext context, { Key? key, required bool? value, - required void Function(bool?)? onChanged, + required ValueChanged? onChanged, bool tristate = false, double tapAreaSizeValue = 40, Color? activeColor, Color? borderColor, Color? checkColor, - Color? fillColor, + Color? inactiveColor, FocusNode? focusNode, bool autofocus = false, required String label, @@ -111,7 +115,7 @@ class MoonCheckbox extends StatefulWidget { activeColor: activeColor, borderColor: borderColor, checkColor: checkColor, - fillColor: fillColor, + inactiveColor: inactiveColor, focusNode: focusNode, autofocus: autofocus, ), @@ -186,12 +190,12 @@ class _MoonCheckboxState extends State with TickerProviderStateMix final Color effectiveActiveColor = widget.activeColor ?? context.moonTheme?.checkboxTheme.colors.activeColor ?? MoonColors.light.piccolo; + final Color effectiveInactiveColor = + widget.inactiveColor ?? context.moonTheme?.checkboxTheme.colors.inactiveColor ?? Colors.transparent; + final Color effectiveCheckColor = widget.checkColor ?? context.moonTheme?.checkboxTheme.colors.checkColor ?? MoonColors.light.goten; - final Color effectiveFillColor = - widget.fillColor ?? context.moonTheme?.checkboxTheme.colors.fillColor ?? Colors.transparent; - final Color effectiveBorderColor = widget.borderColor ?? context.moonTheme?.checkboxTheme.colors.borderColor ?? MoonColors.light.trunks; @@ -245,19 +249,8 @@ class _MoonCheckboxState extends State with TickerProviderStateMix size: size, painter: _painter ..position = position - ..reaction = reaction - ..reactionFocusFade = reactionFocusFade - ..reactionHoverFade = reactionHoverFade - ..inactiveReactionColor = Colors.transparent - ..reactionColor = Colors.transparent - ..hoverColor = Colors.transparent - ..focusColor = effectiveFocusEffectColor - ..splashRadius = 0 - ..downPosition = downPosition - ..isFocused = states.contains(MaterialState.focused) - ..isHovered = states.contains(MaterialState.hovered) ..activeColor = effectiveActiveColor - ..inactiveColor = effectiveFillColor + ..inactiveColor = effectiveInactiveColor ..checkColor = effectiveCheckColor ..value = value ..previousValue = _previousValue diff --git a/lib/src/widgets/checkbox/checkbox_painter.dart b/lib/src/widgets/checkbox/checkbox_painter.dart index 7c4bacc6..67a7fc11 100644 --- a/lib/src/widgets/checkbox/checkbox_painter.dart +++ b/lib/src/widgets/checkbox/checkbox_painter.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; -const double _kEdgeSize = 16; -const double _kStrokeWidth = 1.0; - class MoonCheckboxPainter extends ToggleablePainter { + static const double _kEdgeSize = 16; + static const double _kStrokeWidth = 1.0; + Color get checkColor => _checkColor!; Color? _checkColor; set checkColor(Color value) { @@ -54,14 +54,8 @@ class MoonCheckboxPainter extends ToggleablePainter { notifyListeners(); } - // The square outer bounds of the checkbox at t, with the specified origin. - // At t == 0.0, the outer rect's size is _kEdgeSize (Checkbox.width) - // At t == 0.5, .. is _kEdgeSize - _kStrokeWidth - // At t == 1.0, .. is _kEdgeSize Rect _outerRectAt(Offset origin, double t) { - final double inset = 1.0 - (t - 0.5).abs() * 2.0; - final double size = _kEdgeSize - inset * _kStrokeWidth; - final Rect rect = Rect.fromLTWH(origin.dx + inset, origin.dy + inset, size, size); + final Rect rect = Rect.fromLTWH(origin.dx, origin.dy, _kEdgeSize, _kEdgeSize); return rect; } @@ -142,7 +136,7 @@ class MoonCheckboxPainter extends ToggleablePainter { final Paint paint = Paint()..color = _colorAt(t); if (t <= 0.5) { - final BorderSide border = side ?? BorderSide(width: 2, color: paint.color); + final BorderSide border = side ?? BorderSide(color: paint.color); _drawBox(canvas, outer, paint, border, true); // only paint the border } else { _drawBox(canvas, outer, paint, side, true); diff --git a/lib/src/widgets/radio/radio.dart b/lib/src/widgets/radio/radio.dart new file mode 100644 index 00000000..1987d153 --- /dev/null +++ b/lib/src/widgets/radio/radio.dart @@ -0,0 +1,251 @@ +import 'package:flutter/material.dart'; + +import 'package:moon_design/src/theme/colors.dart'; +import 'package:moon_design/src/theme/effects/focus_effects.dart'; +import 'package:moon_design/src/theme/opacity.dart'; +import 'package:moon_design/src/theme/theme.dart'; +import 'package:moon_design/src/utils/touch_target_padding.dart'; +import 'package:moon_design/src/widgets/common/effects/focus_effect.dart'; +import 'package:moon_design/src/widgets/radio/radio_painter.dart'; + +class MoonRadio extends StatefulWidget { + /// The value represented by this radio button. + final T value; + + /// The currently selected value for a group of radio buttons. + /// + /// This radio button is considered selected if its [value] matches the + /// [groupValue]. + final T? groupValue; + + /// Called when the user selects this radio button. + /// + /// The radio button passes [value] as a parameter to this callback. The radio + /// button does not actually change state until the parent widget rebuilds the + /// radio button with the new [groupValue]. + /// + /// If null, the radio button will be displayed as disabled. + /// + /// The provided callback will not be invoked if this radio button is already + /// selected. + /// + /// The callback provided to [onChanged] should update the state of the parent + /// [StatefulWidget] using the [State.setState] method, so that the parent + /// gets rebuilt. + final ValueChanged? onChanged; + + /// Set to true if this radio button is allowed to be returned to an + /// indeterminate state by selecting it again when selected. + /// + /// To indicate returning to an indeterminate state, [onChanged] will be + /// called with null. + /// + /// If true, [onChanged] can be called with [value] when selected while + /// [groupValue] != [value], or with null when selected again while + /// [groupValue] == [value]. + /// + /// If false, [onChanged] will be called with [value] when it is selected + /// while [groupValue] != [value], and only by selecting another radio button + /// in the group (i.e. changing the value of [groupValue]) can this radio + /// button be unselected. + /// + /// Defaults to false. + final bool toggleable; + + /// The size of the radio's tap target. + /// + /// Defaults to 40. + final double tapAreaSizeValue; + + /// The color to use when this radio is checked. + final Color? activeColor; + + /// The color to use for the radio's background when the radio is not checked. + final Color? inactiveColor; + + /// {@macro flutter.widgets.Focus.focusNode}. + final FocusNode? focusNode; + + /// {@macro flutter.widgets.Focus.autofocus} + final bool autofocus; + + /// MDS radio widget. + const MoonRadio({ + super.key, + required this.value, + required this.groupValue, + required this.onChanged, + this.toggleable = false, + this.tapAreaSizeValue = 40, + this.activeColor, + this.inactiveColor, + this.focusNode, + this.autofocus = false, + }); + + static Widget withLabel( + BuildContext context, { + Key? key, + required T value, + required T? groupValue, + required ValueChanged? onChanged, + bool toggleable = false, + double tapAreaSizeValue = 40, + Color? activeColor, + Color? inactiveColor, + FocusNode? focusNode, + bool autofocus = false, + required String label, + }) { + final Color effectiveTextColor = context.moonColors?.bulma ?? MoonColors.light.bulma; + + final TextStyle effectiveTextStyle = + context.moonTheme?.typography.body.text14.copyWith(color: effectiveTextColor) ?? + TextStyle(fontSize: 14, color: effectiveTextColor); + + final Duration effectiveFocusEffectDuration = + context.moonEffects?.controlFocusEffect.effectDuration ?? MoonFocusEffects.lightFocusEffect.effectDuration; + + final double effectiveDisabledOpacityValue = context.moonTheme?.opacity.disabled ?? MoonOpacity.opacities.disabled; + + final bool isInteractive = onChanged != null; + + return GestureDetector( + onTap: () => onChanged?.call(value), + child: MouseRegion( + cursor: isInteractive ? SystemMouseCursors.click : SystemMouseCursors.basic, + child: ConstrainedBox( + constraints: BoxConstraints(minHeight: tapAreaSizeValue), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + MoonRadio( + key: key, + value: value, + groupValue: groupValue, + onChanged: onChanged, + toggleable: toggleable, + tapAreaSizeValue: 0, + activeColor: activeColor, + inactiveColor: inactiveColor, + focusNode: focusNode, + autofocus: autofocus, + ), + const SizedBox(width: 12), + AnimatedOpacity( + opacity: isInteractive ? 1 : effectiveDisabledOpacityValue, + duration: effectiveFocusEffectDuration, + child: Text( + label, + style: effectiveTextStyle, + ), + ), + ], + ), + ), + ), + ); + } + + bool get _selected => value == groupValue; + + @override + State> createState() => _RadioState(); +} + +class _RadioState extends State> with TickerProviderStateMixin, ToggleableStateMixin { + final MoonRadioPainter _painter = MoonRadioPainter(); + + void _handleChanged(bool? selected) { + if (selected == null) { + widget.onChanged!(null); + return; + } + if (selected) { + widget.onChanged!(widget.value); + } + } + + @override + void didUpdateWidget(MoonRadio oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget._selected != oldWidget._selected) { + animateToValue(); + } + } + + @override + void dispose() { + _painter.dispose(); + super.dispose(); + } + + @override + ValueChanged? get onChanged => widget.onChanged != null ? _handleChanged : null; + + @override + bool get tristate => widget.toggleable; + + @override + bool? get value => widget._selected; + + @override + Widget build(BuildContext context) { + const Size size = Size(16, 16); + + final Color effectiveActiveColor = + widget.activeColor ?? context.moonTheme?.radioTheme.colors.activeColor ?? MoonColors.light.piccolo; + + final Color effectiveInactiveColor = + widget.inactiveColor ?? context.moonTheme?.radioTheme.colors.inactiveColor ?? MoonColors.light.trunks; + + final double effectiveFocusEffectExtent = + context.moonEffects?.controlFocusEffect.effectExtent ?? MoonFocusEffects.lightFocusEffect.effectExtent; + + final Color effectiveFocusEffectColor = + context.moonEffects?.controlFocusEffect.effectColor ?? MoonFocusEffects.lightFocusEffect.effectColor; + + final Curve effectiveFocusEffectCurve = + context.moonEffects?.controlFocusEffect.effectCurve ?? MoonFocusEffects.lightFocusEffect.effectCurve; + + final Duration effectiveFocusEffectDuration = + context.moonEffects?.controlFocusEffect.effectDuration ?? MoonFocusEffects.lightFocusEffect.effectDuration; + + final double effectiveDisabledOpacityValue = context.moonTheme?.opacity.disabled ?? MoonOpacity.opacities.disabled; + + final MaterialStateProperty effectiveMouseCursor = + MaterialStateProperty.resolveWith((Set states) { + return MaterialStateMouseCursor.clickable.resolve(states); + }); + + return Semantics( + inMutuallyExclusiveGroup: true, + checked: widget._selected, + child: TouchTargetPadding( + minSize: Size(widget.tapAreaSizeValue, widget.tapAreaSizeValue), + child: MoonFocusEffect( + show: states.contains(MaterialState.focused), + effectExtent: effectiveFocusEffectExtent, + childBorderRadius: BorderRadius.circular(8), + effectColor: effectiveFocusEffectColor, + effectCurve: effectiveFocusEffectCurve, + effectDuration: effectiveFocusEffectDuration, + child: AnimatedOpacity( + opacity: states.contains(MaterialState.disabled) ? effectiveDisabledOpacityValue : 1, + duration: effectiveFocusEffectDuration, + child: buildToggleable( + focusNode: widget.focusNode, + autofocus: widget.autofocus, + mouseCursor: effectiveMouseCursor, + size: size, + painter: _painter + ..position = position + ..activeColor = effectiveActiveColor + ..inactiveColor = effectiveInactiveColor, + ), + ), + ), + ), + ); + } +} diff --git a/lib/src/widgets/radio/radio_painter.dart b/lib/src/widgets/radio/radio_painter.dart new file mode 100644 index 00000000..9b0e2611 --- /dev/null +++ b/lib/src/widgets/radio/radio_painter.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; + +class MoonRadioPainter extends ToggleablePainter { + static const double _kOuterRadius = 8.0; + static const double _kInnerRadius = 4.0; + + @override + void paint(Canvas canvas, Size size) { + final Offset center = (Offset.zero & size).center; + + // Outer circle + final Paint paint = Paint() + ..color = Color.lerp(inactiveColor, activeColor, position.value)! + ..style = PaintingStyle.stroke + ..strokeWidth = 1.0; + canvas.drawCircle(center, _kOuterRadius, paint); + + // Inner circle + if (!position.isDismissed) { + paint.style = PaintingStyle.fill; + canvas.drawCircle(center, _kInnerRadius * position.value, paint); + } + } +}