From 56999a465a7822a9c150539a7d63fbdb59a37e79 Mon Sep 17 00:00:00 2001 From: Kypsis Date: Wed, 15 Mar 2023 15:12:30 +0200 Subject: [PATCH 1/5] Create Checkbox scaffolding --- .../lib/src/storybook/stories/checkbox.dart | 74 +++++++++++++++++++ example/lib/src/storybook/storybook.dart | 4 +- lib/moon_design.dart | 1 + lib/src/widgets/checkbox/checkbox.dart | 20 +++++ 4 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 example/lib/src/storybook/stories/checkbox.dart create mode 100644 lib/src/widgets/checkbox/checkbox.dart diff --git a/example/lib/src/storybook/stories/checkbox.dart b/example/lib/src/storybook/stories/checkbox.dart new file mode 100644 index 00000000..70f0dbb3 --- /dev/null +++ b/example/lib/src/storybook/stories/checkbox.dart @@ -0,0 +1,74 @@ +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'; + +bool? value = false; + +class CheckboxStory extends Story { + CheckboxStory() + : super( + name: "Checkbox", + builder: (context) { + final thumbColorsKnob = context.knobs.options( + label: "thumbColor", + description: "MoonColors variants for the Checkbox thumb.", + initial: 7, // goten + options: colorOptions, + ); + + final thumbColor = colorTable(context)[thumbColorsKnob]; + + final activeTrackColorsKnob = context.knobs.options( + label: "activeTrackColor", + description: "MoonColors variants for the active Checkbox track.", + initial: 0, // piccolo + options: colorOptions, + ); + + final activeTrackColor = colorTable(context)[activeTrackColorsKnob]; + + final inactiveTrackColorsKnob = context.knobs.options( + label: "inactiveTrackColor", + description: "MoonColors variants for the active Checkbox track.", + initial: 2, // beerus + options: colorOptions, + ); + + final inactiveTrackColor = colorTable(context)[inactiveTrackColorsKnob]; + + final isDisabled = context.knobs.boolean( + label: "Disabled", + description: "onChanged() is null.", + ); + + final setRtlModeKnob = context.knobs.boolean( + label: "RTL mode", + description: "Checkbox between LTR and RTL modes.", + ); + + return Directionality( + textDirection: setRtlModeKnob ? TextDirection.rtl : TextDirection.ltr, + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: 64), + StatefulBuilder( + builder: (context, setState) { + return MoonCheckbox( + value: value, + onChanged: isDisabled ? null : (newValue) => setState(() => value = newValue), + ); + }, + ), + const SizedBox(height: 64), + ], + ), + ), + ); + }, + ); +} diff --git a/example/lib/src/storybook/storybook.dart b/example/lib/src/storybook/storybook.dart index 91cbde29..8379fc0d 100644 --- a/example/lib/src/storybook/storybook.dart +++ b/example/lib/src/storybook/storybook.dart @@ -1,6 +1,7 @@ import 'package:example/src/storybook/common/widgets/version.dart'; import 'package:example/src/storybook/stories/avatar.dart'; import 'package:example/src/storybook/stories/button.dart'; +import 'package:example/src/storybook/stories/checkbox.dart'; import 'package:example/src/storybook/stories/chip.dart'; import 'package:example/src/storybook/stories/circular_loader.dart'; import 'package:example/src/storybook/stories/circular_progress.dart'; @@ -33,7 +34,7 @@ class StorybookPage extends StatelessWidget { return Stack( children: [ Storybook( - initialStory: "Avatar", + initialStory: "Checkbox", plugins: _plugins, wrapperBuilder: (context, child) => MaterialApp( title: "Moon Design for Flutter", @@ -67,6 +68,7 @@ class StorybookPage extends StatelessWidget { stories: [ AvatarStory(), ButtonStory(), + CheckboxStory(), ChipStory(), CircularLoaderStory(), CircularProgressStory(), diff --git a/lib/moon_design.dart b/lib/moon_design.dart index 8b81991c..63593a73 100644 --- a/lib/moon_design.dart +++ b/lib/moon_design.dart @@ -31,6 +31,7 @@ export 'package:moon_design/src/widgets/buttons/ghost_button.dart'; export 'package:moon_design/src/widgets/buttons/primary_button.dart'; export 'package:moon_design/src/widgets/buttons/secondary_button.dart'; export 'package:moon_design/src/widgets/buttons/tertiary_button.dart'; +export 'package:moon_design/src/widgets/checkbox/checkbox.dart'; export 'package:moon_design/src/widgets/chips/chip.dart'; export 'package:moon_design/src/widgets/chips/ghost_chip.dart'; export 'package:moon_design/src/widgets/common/animated_icon_theme.dart'; diff --git a/lib/src/widgets/checkbox/checkbox.dart b/lib/src/widgets/checkbox/checkbox.dart new file mode 100644 index 00000000..aad5552f --- /dev/null +++ b/lib/src/widgets/checkbox/checkbox.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; + +class MoonCheckbox extends StatelessWidget { + /// Determines if the switch is on or off. + final bool? value; + + /// Callback when the switch is toggled on or off. + final ValueChanged? onChanged; + + const MoonCheckbox({ + super.key, + required this.value, + required this.onChanged, + }); + + @override + Widget build(BuildContext context) { + return Checkbox(value: value, onChanged: onChanged); + } +} From c31e99b17b0caf24b43e07aa05a71c6ff25d4da4 Mon Sep 17 00:00:00 2001 From: Kypsis Date: Wed, 15 Mar 2023 15:48:17 +0200 Subject: [PATCH 2/5] Create Checkbox theming --- lib/src/theme/checkbox/checkbox_colors.dart | 78 +++++++++++++++++++ .../theme/checkbox/checkbox_properties.dart | 45 +++++++++++ lib/src/theme/checkbox/checkbox_theme.dart | 59 ++++++++++++++ lib/src/theme/theme.dart | 11 +++ lib/src/widgets/checkbox/checkbox.dart | 8 +- 5 files changed, 199 insertions(+), 2 deletions(-) create mode 100644 lib/src/theme/checkbox/checkbox_colors.dart create mode 100644 lib/src/theme/checkbox/checkbox_properties.dart create mode 100644 lib/src/theme/checkbox/checkbox_theme.dart diff --git a/lib/src/theme/checkbox/checkbox_colors.dart b/lib/src/theme/checkbox/checkbox_colors.dart new file mode 100644 index 00000000..4750d4d5 --- /dev/null +++ b/lib/src/theme/checkbox/checkbox_colors.dart @@ -0,0 +1,78 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'package:moon_design/src/theme/colors.dart'; + +@immutable +class MoonCheckboxColors extends ThemeExtension with DiagnosticableTreeMixin { + static final light = MoonCheckboxColors( + activeColor: MoonColors.light.piccolo, + fillColor: Colors.transparent, + iconColor: MoonColors.light.goten, + borderColor: MoonColors.light.trunks, + ); + + static final dark = MoonCheckboxColors( + activeColor: MoonColors.dark.piccolo, + fillColor: Colors.transparent, + iconColor: MoonColors.dark.goten, + borderColor: MoonColors.dark.trunks, + ); + + /// Checkbox active color. + final Color activeColor; + + /// Checkbox border color. + final Color borderColor; + + /// Checkbox fill (inactive) color. + final Color fillColor; + + /// Checkbox icon color. + final Color iconColor; + + const MoonCheckboxColors({ + required this.activeColor, + required this.borderColor, + required this.fillColor, + required this.iconColor, + }); + + @override + MoonCheckboxColors copyWith({ + Color? activeColor, + Color? borderColor, + Color? fillColor, + Color? iconColor, + }) { + return MoonCheckboxColors( + activeColor: activeColor ?? this.activeColor, + borderColor: borderColor ?? this.borderColor, + fillColor: fillColor ?? this.fillColor, + iconColor: iconColor ?? this.iconColor, + ); + } + + @override + MoonCheckboxColors lerp(ThemeExtension? other, double t) { + if (other is! MoonCheckboxColors) return this; + + return MoonCheckboxColors( + activeColor: Color.lerp(activeColor, other.activeColor, t)!, + borderColor: Color.lerp(borderColor, other.borderColor, t)!, + fillColor: Color.lerp(fillColor, other.fillColor, t)!, + iconColor: Color.lerp(iconColor, other.iconColor, t)!, + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty("type", "MoonCheckboxColors")) + ..add(ColorProperty("activeColor", activeColor)) + ..add(ColorProperty("borderColor", borderColor)) + ..add(ColorProperty("fillColor", fillColor)) + ..add(ColorProperty("iconColor", iconColor)); + } +} diff --git a/lib/src/theme/checkbox/checkbox_properties.dart b/lib/src/theme/checkbox/checkbox_properties.dart new file mode 100644 index 00000000..94804a67 --- /dev/null +++ b/lib/src/theme/checkbox/checkbox_properties.dart @@ -0,0 +1,45 @@ +import 'dart:ui'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:moon_design/src/theme/borders.dart'; + +@immutable +class MoonCheckboxProperties extends ThemeExtension with DiagnosticableTreeMixin { + static final properties = MoonCheckboxProperties( + borderRadiusValue: MoonBorders.borders.interactiveXs.topLeft.x, + ); + + /// Checkbox border radius value. + final double borderRadiusValue; + + const MoonCheckboxProperties({ + required this.borderRadiusValue, + }); + + @override + MoonCheckboxProperties copyWith({ + double? borderRadiusValue, + }) { + return MoonCheckboxProperties( + borderRadiusValue: borderRadiusValue ?? this.borderRadiusValue, + ); + } + + @override + MoonCheckboxProperties lerp(ThemeExtension? other, double t) { + if (other is! MoonCheckboxProperties) return this; + + return MoonCheckboxProperties( + borderRadiusValue: lerpDouble(borderRadiusValue, other.borderRadiusValue, t)!, + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty("type", "MoonCheckboxProperties")) + ..add(DiagnosticsProperty("borderRadiusValue", borderRadiusValue)); + } +} diff --git a/lib/src/theme/checkbox/checkbox_theme.dart b/lib/src/theme/checkbox/checkbox_theme.dart new file mode 100644 index 00000000..223ba460 --- /dev/null +++ b/lib/src/theme/checkbox/checkbox_theme.dart @@ -0,0 +1,59 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'package:moon_design/src/theme/checkbox/checkbox_colors.dart'; +import 'package:moon_design/src/theme/checkbox/checkbox_properties.dart'; + +@immutable +class MoonCheckboxTheme extends ThemeExtension with DiagnosticableTreeMixin { + static final light = MoonCheckboxTheme( + colors: MoonCheckboxColors.light, + properties: MoonCheckboxProperties.properties, + ); + + static final dark = MoonCheckboxTheme( + colors: MoonCheckboxColors.dark, + properties: MoonCheckboxProperties.properties, + ); + + /// Checkbox colors. + final MoonCheckboxColors colors; + + /// Checkbox properties. + final MoonCheckboxProperties properties; + + const MoonCheckboxTheme({ + required this.colors, + required this.properties, + }); + + @override + MoonCheckboxTheme copyWith({ + MoonCheckboxColors? colors, + MoonCheckboxProperties? properties, + }) { + return MoonCheckboxTheme( + colors: colors ?? this.colors, + properties: properties ?? this.properties, + ); + } + + @override + MoonCheckboxTheme lerp(ThemeExtension? other, double t) { + if (other is! MoonCheckboxTheme) return this; + + return MoonCheckboxTheme( + colors: colors.lerp(other.colors, t), + properties: properties.lerp(other.properties, t), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder diagnosticProperties) { + super.debugFillProperties(diagnosticProperties); + diagnosticProperties + ..add(DiagnosticsProperty("type", "MoonCheckboxTheme")) + ..add(DiagnosticsProperty("colors", colors)) + ..add(DiagnosticsProperty("properties", properties)); + } +} diff --git a/lib/src/theme/theme.dart b/lib/src/theme/theme.dart index 0210ff33..6cdafb67 100644 --- a/lib/src/theme/theme.dart +++ b/lib/src/theme/theme.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.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'; +import 'package:moon_design/src/theme/checkbox/checkbox_theme.dart'; import 'package:moon_design/src/theme/chip/chip_theme.dart'; import 'package:moon_design/src/theme/colors.dart'; import 'package:moon_design/src/theme/effects/effects.dart'; @@ -26,6 +27,7 @@ class MoonTheme extends ThemeExtension with DiagnosticableTreeMixin { avatarTheme: MoonAvatarTheme.light, borders: MoonBorders.borders, buttonTheme: MoonButtonTheme.light, + checkboxTheme: MoonCheckboxTheme.light, chipTheme: MoonChipTheme.light, circularLoaderTheme: MoonCircularLoaderTheme.light, circularProgressTheme: MoonCircularProgressTheme.light, @@ -47,6 +49,7 @@ class MoonTheme extends ThemeExtension with DiagnosticableTreeMixin { avatarTheme: MoonAvatarTheme.dark, borders: MoonBorders.borders, buttonTheme: MoonButtonTheme.dark, + checkboxTheme: MoonCheckboxTheme.dark, chipTheme: MoonChipTheme.dark, circularLoaderTheme: MoonCircularLoaderTheme.dark, circularProgressTheme: MoonCircularProgressTheme.dark, @@ -73,6 +76,9 @@ class MoonTheme extends ThemeExtension with DiagnosticableTreeMixin { /// Moon Design System MoonButton widgets theming. final MoonButtonTheme buttonTheme; + /// Moon Design System MoonCheckbox widget theming. + final MoonCheckboxTheme checkboxTheme; + /// Moon Design System MoonChip widgets theming. final MoonChipTheme chipTheme; @@ -122,6 +128,7 @@ class MoonTheme extends ThemeExtension with DiagnosticableTreeMixin { required this.avatarTheme, required this.borders, required this.buttonTheme, + required this.checkboxTheme, required this.chipTheme, required this.circularLoaderTheme, required this.circularProgressTheme, @@ -144,6 +151,7 @@ class MoonTheme extends ThemeExtension with DiagnosticableTreeMixin { MoonAvatarTheme? avatarTheme, MoonBorders? borders, MoonButtonTheme? buttonTheme, + MoonCheckboxTheme? checkboxTheme, MoonChipTheme? chipTheme, MoonCircularLoaderTheme? circularLoaderTheme, MoonCircularProgressTheme? circularProgressTheme, @@ -164,6 +172,7 @@ class MoonTheme extends ThemeExtension with DiagnosticableTreeMixin { avatarTheme: avatarTheme ?? this.avatarTheme, borders: borders ?? this.borders, buttonTheme: buttonTheme ?? this.buttonTheme, + checkboxTheme: checkboxTheme ?? this.checkboxTheme, chipTheme: chipTheme ?? this.chipTheme, circularLoaderTheme: circularLoaderTheme ?? this.circularLoaderTheme, circularProgressTheme: circularProgressTheme ?? this.circularProgressTheme, @@ -190,6 +199,7 @@ class MoonTheme extends ThemeExtension with DiagnosticableTreeMixin { avatarTheme: avatarTheme.lerp(other.avatarTheme, t), borders: borders.lerp(other.borders, t), buttonTheme: buttonTheme.lerp(other.buttonTheme, t), + checkboxTheme: checkboxTheme.lerp(other.checkboxTheme, t), chipTheme: chipTheme.lerp(other.chipTheme, t), circularLoaderTheme: circularLoaderTheme.lerp(other.circularLoaderTheme, t), circularProgressTheme: circularProgressTheme.lerp(other.circularProgressTheme, t), @@ -216,6 +226,7 @@ class MoonTheme extends ThemeExtension with DiagnosticableTreeMixin { ..add(DiagnosticsProperty("MoonAvatarTheme", avatarTheme)) ..add(DiagnosticsProperty("MoonBorders", borders)) ..add(DiagnosticsProperty("MoonButtonTheme", buttonTheme)) + ..add(DiagnosticsProperty("MoonCheckboxTheme", checkboxTheme)) ..add(DiagnosticsProperty("MoonChipTheme", chipTheme)) ..add(DiagnosticsProperty("MoonCircularLoaderTheme", circularLoaderTheme)) ..add(DiagnosticsProperty("MoonCircularProgressTheme", circularProgressTheme)) diff --git a/lib/src/widgets/checkbox/checkbox.dart b/lib/src/widgets/checkbox/checkbox.dart index aad5552f..95ce2ad3 100644 --- a/lib/src/widgets/checkbox/checkbox.dart +++ b/lib/src/widgets/checkbox/checkbox.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; class MoonCheckbox extends StatelessWidget { - /// Determines if the switch is on or off. + /// Determines if the checkbox is toggled on or off. final bool? value; /// Callback when the switch is toggled on or off. @@ -15,6 +15,10 @@ class MoonCheckbox extends StatelessWidget { @override Widget build(BuildContext context) { - return Checkbox(value: value, onChanged: onChanged); + return Checkbox( + value: value, + onChanged: onChanged, + splashRadius: 0, + ); } } From f2125bfff8f547d1e036a3371403ff79a9c990e7 Mon Sep 17 00:00:00 2001 From: Kypsis Date: Wed, 15 Mar 2023 20:01:12 +0200 Subject: [PATCH 3/5] Finalize Checkbox wrapper approach --- lib/moon_design.dart | 1 + lib/src/theme/checkbox/checkbox_colors.dart | 22 ++--- lib/src/widgets/checkbox/checkbox.dart | 99 +++++++++++++++++++-- 3 files changed, 105 insertions(+), 17 deletions(-) diff --git a/lib/moon_design.dart b/lib/moon_design.dart index 63593a73..33d998c9 100644 --- a/lib/moon_design.dart +++ b/lib/moon_design.dart @@ -3,6 +3,7 @@ library moon_design; export 'package:moon_design/src/theme/avatar/avatar_theme.dart'; export 'package:moon_design/src/theme/borders.dart'; export 'package:moon_design/src/theme/button/button_theme.dart'; +export 'package:moon_design/src/theme/checkbox/checkbox_theme.dart'; export 'package:moon_design/src/theme/chip/chip_theme.dart'; export 'package:moon_design/src/theme/colors.dart'; export 'package:moon_design/src/theme/effects/effects.dart'; diff --git a/lib/src/theme/checkbox/checkbox_colors.dart b/lib/src/theme/checkbox/checkbox_colors.dart index 4750d4d5..ad209d1c 100644 --- a/lib/src/theme/checkbox/checkbox_colors.dart +++ b/lib/src/theme/checkbox/checkbox_colors.dart @@ -7,15 +7,15 @@ import 'package:moon_design/src/theme/colors.dart'; class MoonCheckboxColors extends ThemeExtension with DiagnosticableTreeMixin { static final light = MoonCheckboxColors( activeColor: MoonColors.light.piccolo, + checkColor: MoonColors.light.goten, fillColor: Colors.transparent, - iconColor: MoonColors.light.goten, borderColor: MoonColors.light.trunks, ); static final dark = MoonCheckboxColors( activeColor: MoonColors.dark.piccolo, + checkColor: MoonColors.dark.goten, fillColor: Colors.transparent, - iconColor: MoonColors.dark.goten, borderColor: MoonColors.dark.trunks, ); @@ -25,31 +25,31 @@ class MoonCheckboxColors extends ThemeExtension with Diagnos /// Checkbox border color. final Color borderColor; + /// Checkbox icon color. + final Color checkColor; + /// Checkbox fill (inactive) color. final Color fillColor; - /// Checkbox icon color. - final Color iconColor; - const MoonCheckboxColors({ required this.activeColor, required this.borderColor, + required this.checkColor, required this.fillColor, - required this.iconColor, }); @override MoonCheckboxColors copyWith({ Color? activeColor, Color? borderColor, + Color? checkColor, Color? fillColor, - Color? iconColor, }) { return MoonCheckboxColors( activeColor: activeColor ?? this.activeColor, borderColor: borderColor ?? this.borderColor, + checkColor: checkColor ?? this.checkColor, fillColor: fillColor ?? this.fillColor, - iconColor: iconColor ?? this.iconColor, ); } @@ -60,8 +60,8 @@ class MoonCheckboxColors extends ThemeExtension with Diagnos return MoonCheckboxColors( 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)!, - iconColor: Color.lerp(iconColor, other.iconColor, t)!, ); } @@ -72,7 +72,7 @@ class MoonCheckboxColors extends ThemeExtension with Diagnos ..add(DiagnosticsProperty("type", "MoonCheckboxColors")) ..add(ColorProperty("activeColor", activeColor)) ..add(ColorProperty("borderColor", borderColor)) - ..add(ColorProperty("fillColor", fillColor)) - ..add(ColorProperty("iconColor", iconColor)); + ..add(ColorProperty("checkColor", checkColor)) + ..add(ColorProperty("fillColor", fillColor)); } } diff --git a/lib/src/widgets/checkbox/checkbox.dart b/lib/src/widgets/checkbox/checkbox.dart index 95ce2ad3..02fd5602 100644 --- a/lib/src/widgets/checkbox/checkbox.dart +++ b/lib/src/widgets/checkbox/checkbox.dart @@ -1,24 +1,111 @@ +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/effects/focus_effects.dart'; class MoonCheckbox extends StatelessWidget { - /// Determines if the checkbox is toggled on or off. + /// Whether this checkbox is checked. + /// + /// When [tristate] is true, a value of null corresponds to the mixed state. + /// When [tristate] is false, this value must not be null. final bool? value; - /// Callback when the switch is toggled on or off. + /// Callback for when the checkbox value changes. final ValueChanged? onChanged; + /// If true the checkbox's [value] can be true, false, or null. + /// + /// [Checkbox] displays a dash when its value is null. + /// + /// When a tri-state checkbox ([tristate] is true) is tapped, its [onChanged] + /// callback will be applied to true if the current value is false, to null if + /// value is true, and to false if value is null (i.e. it cycles through false + /// => true => null => false when tapped). + /// + /// If tristate is false (the default), [value] must not be null. + final bool tristate; + + /// The color to use when this checkbox is checked. + final Color? activeColor; + + /// The color to use for the check icon when this checkbox is checked. + final Color? checkColor; + + /// The color to use for the checkbox's background when the checkbox is not checked. + final Color? fillColor; + + /// The focus node for the checkbox. + final FocusNode? focusNode; + const MoonCheckbox({ super.key, required this.value, required this.onChanged, + this.tristate = false, + this.activeColor, + this.checkColor, + this.fillColor, + this.focusNode, }); @override Widget build(BuildContext context) { - return Checkbox( - value: value, - onChanged: onChanged, - splashRadius: 0, + final Color effectiveActiveColor = + activeColor ?? context.moonTheme?.checkboxTheme.colors.activeColor ?? MoonColors.light.piccolo; + + final Color effectiveCheckColor = + activeColor ?? context.moonTheme?.checkboxTheme.colors.checkColor ?? MoonColors.light.goten; + + final Color effectiveFillColor = + activeColor ?? context.moonTheme?.checkboxTheme.colors.activeColor ?? MoonColors.light.piccolo; + + final Color effectiveFocusEffectColor = + context.moonEffects?.controlFocusEffect.effectColor ?? MoonFocusEffects.lightFocusEffect.effectColor; + + final Color effectiveBorderColor = + activeColor ?? context.moonTheme?.checkboxTheme.colors.borderColor ?? MoonColors.light.trunks; + + final MaterialStateProperty resolvedFillColor = + MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) { + return effectiveFillColor.withOpacity(.32); + } + + if (states.contains(MaterialState.focused)) { + return effectiveFocusEffectColor; + } + + return effectiveFillColor; + }); + + final double effectiveBorderRadiusValue = context.moonTheme?.checkboxTheme.properties.borderRadiusValue ?? 4; + + return SizedBox( + height: 28, + width: 28, + child: FittedBox( + child: Checkbox( + value: value, + onChanged: onChanged, + tristate: tristate, + focusNode: focusNode, + activeColor: effectiveActiveColor, + checkColor: effectiveCheckColor, + fillColor: resolvedFillColor, + focusColor: effectiveFocusEffectColor, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + side: BorderSide(color: effectiveBorderColor, width: 1.25), + shape: SmoothRectangleBorder( + borderRadius: SmoothBorderRadius.all( + SmoothRadius( + cornerRadius: effectiveBorderRadiusValue, + cornerSmoothing: 1, + ), + ), + ), + splashRadius: 0, + ), + ), ); } } From 603c919e603e48b3d2a13ad7e7101229c09f37f6 Mon Sep 17 00:00:00 2001 From: Kypsis Date: Thu, 16 Mar 2023 00:18:14 +0200 Subject: [PATCH 4/5] Finalise Checkbox --- .../lib/src/storybook/stories/checkbox.dart | 56 +++- example/lib/src/storybook/stories/switch.dart | 5 +- example/lib/src/storybook/stories/tag.dart | 1 + lib/src/widgets/avatar/avatar.dart | 1 + lib/src/widgets/buttons/button.dart | 4 +- lib/src/widgets/buttons/ghost_button.dart | 4 +- lib/src/widgets/buttons/primary_button.dart | 4 +- lib/src/widgets/buttons/secondary_button.dart | 4 +- lib/src/widgets/buttons/tertiary_button.dart | 4 +- lib/src/widgets/checkbox/checkbox.dart | 241 +++++++++++++++--- .../widgets/checkbox/checkbox_painter.dart | 179 +++++++++++++ lib/src/widgets/chips/chip.dart | 5 +- lib/src/widgets/chips/ghost_chip.dart | 5 +- lib/src/widgets/common/base_control.dart | 8 +- lib/src/widgets/loaders/circular_loader.dart | 12 + lib/src/widgets/loaders/linear_loader.dart | 10 + lib/src/widgets/popover/popover.dart | 1 + .../widgets/progress/circular_progress.dart | 14 + lib/src/widgets/progress/linear_progress.dart | 12 + lib/src/widgets/switch/switch.dart | 27 +- lib/src/widgets/tag/tag.dart | 96 +++---- lib/src/widgets/tooltip/tooltip.dart | 1 + 22 files changed, 565 insertions(+), 129 deletions(-) create mode 100644 lib/src/widgets/checkbox/checkbox_painter.dart diff --git a/example/lib/src/storybook/stories/checkbox.dart b/example/lib/src/storybook/stories/checkbox.dart index 70f0dbb3..1cd9f6f3 100644 --- a/example/lib/src/storybook/stories/checkbox.dart +++ b/example/lib/src/storybook/stories/checkbox.dart @@ -12,41 +12,46 @@ class CheckboxStory extends Story { : super( name: "Checkbox", builder: (context) { - final thumbColorsKnob = context.knobs.options( - label: "thumbColor", - description: "MoonColors variants for the Checkbox thumb.", + final checkColorsKnob = context.knobs.options( + label: "checkColor", + description: "MoonColors variants for the Checkbox icon.", initial: 7, // goten options: colorOptions, ); - final thumbColor = colorTable(context)[thumbColorsKnob]; + final checkColor = colorTable(context)[checkColorsKnob]; - final activeTrackColorsKnob = context.knobs.options( - label: "activeTrackColor", - description: "MoonColors variants for the active Checkbox track.", + final activeColorsKnob = context.knobs.options( + label: "activeColor", + description: "MoonColors variants for when Checkbox is checked.", initial: 0, // piccolo options: colorOptions, ); - final activeTrackColor = colorTable(context)[activeTrackColorsKnob]; + final activeColor = colorTable(context)[activeColorsKnob]; - final inactiveTrackColorsKnob = context.knobs.options( - label: "inactiveTrackColor", - description: "MoonColors variants for the active Checkbox track.", - initial: 2, // beerus + final fillColorsKnob = context.knobs.options( + label: "fillColor", + description: "MoonColors variants for when Checkbox is unchecked.", + initial: 39, // transparent options: colorOptions, ); - final inactiveTrackColor = colorTable(context)[inactiveTrackColorsKnob]; + final fillColor = colorTable(context)[fillColorsKnob]; final isDisabled = context.knobs.boolean( label: "Disabled", description: "onChanged() is null.", ); + final isTristate = context.knobs.boolean( + label: "tristate", + description: "Whether the checkbox uses tristate.", + ); + final setRtlModeKnob = context.knobs.boolean( label: "RTL mode", - description: "Checkbox between LTR and RTL modes.", + description: "Switch between LTR and RTL modes.", ); return Directionality( @@ -56,11 +61,34 @@ class CheckboxStory extends Story { mainAxisAlignment: MainAxisAlignment.center, children: [ const SizedBox(height: 64), + const TextDivider(text: "Customisable Checkbox"), + const SizedBox(height: 32), StatefulBuilder( builder: (context, setState) { return MoonCheckbox( + checkColor: checkColor, + activeColor: activeColor, + fillColor: fillColor, + tristate: isTristate, + value: value, + onChanged: isDisabled ? null : (newValue) => setState(() => value = newValue), + ); + }, + ), + const SizedBox(height: 40), + const TextDivider(text: "Checkbox with clickable text"), + const SizedBox(height: 32), + StatefulBuilder( + builder: (context, setState) { + return MoonCheckbox.withLabel( + context, + checkColor: checkColor, + activeColor: activeColor, + fillColor: fillColor, + tristate: isTristate, value: value, onChanged: isDisabled ? null : (newValue) => setState(() => value = newValue), + label: "With label", ); }, ), diff --git a/example/lib/src/storybook/stories/switch.dart b/example/lib/src/storybook/stories/switch.dart index d44a3283..d1e411a8 100644 --- a/example/lib/src/storybook/stories/switch.dart +++ b/example/lib/src/storybook/stories/switch.dart @@ -1,6 +1,7 @@ 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/foundation.dart'; import 'package:flutter/material.dart'; import 'package:moon_design/moon_design.dart'; import 'package:storybook_flutter/storybook_flutter.dart'; @@ -97,12 +98,12 @@ class SwitchStory extends Story { ), activeTrackWidget: Text( "ON", - style: MoonTextStyles.caption.text8.copyWith(letterSpacing: 0.5), + style: MoonTextStyles.caption.text8.copyWith(letterSpacing: kIsWeb ? 0.5 : 0.1), textAlign: TextAlign.center, ), inactiveTrackWidget: Text( "OFF", - style: MoonTextStyles.caption.text8.copyWith(letterSpacing: 0.5), + style: MoonTextStyles.caption.text8.copyWith(letterSpacing: kIsWeb ? 0.5 : 0.1), textAlign: TextAlign.center, ), value: value, diff --git a/example/lib/src/storybook/stories/tag.dart b/example/lib/src/storybook/stories/tag.dart index 91b208e9..993b1322 100644 --- a/example/lib/src/storybook/stories/tag.dart +++ b/example/lib/src/storybook/stories/tag.dart @@ -75,6 +75,7 @@ class TagStory extends Story { const SizedBox(height: 64), MoonTag( borderRadius: BorderRadius.circular(borderRadiusKnob.toDouble()), + onTap: () {}, tagSize: tagSizesKnob, isUpperCase: setUpperCase, backgroundColor: color, diff --git a/lib/src/widgets/avatar/avatar.dart b/lib/src/widgets/avatar/avatar.dart index 89563a99..222b7b16 100644 --- a/lib/src/widgets/avatar/avatar.dart +++ b/lib/src/widgets/avatar/avatar.dart @@ -66,6 +66,7 @@ class MoonAvatar extends StatelessWidget { /// The child of the avatar. final Widget? child; + /// MDS avatar widget. const MoonAvatar({ super.key, this.width, diff --git a/lib/src/widgets/buttons/button.dart b/lib/src/widgets/buttons/button.dart index 84411b17..88de353b 100644 --- a/lib/src/widgets/buttons/button.dart +++ b/lib/src/widgets/buttons/button.dart @@ -28,7 +28,7 @@ class MoonButton extends StatelessWidget { /// The size of the button. final MoonButtonSize? buttonSize; - /// The focus node for the button. + /// {@macro flutter.widgets.Focus.focusNode}. final FocusNode? focusNode; /// The semantic label for the button. @@ -64,7 +64,7 @@ class MoonButton extends StatelessWidget { /// The minimum size of the touch target. final double minTouchTargetSize; - /// Whether the button should automatically be focused when it is mounted. + /// {@macro flutter.widgets.Focus.autofocus} final bool autofocus; /// Whether the button should be focusable. diff --git a/lib/src/widgets/buttons/ghost_button.dart b/lib/src/widgets/buttons/ghost_button.dart index 461b3c5b..abf75f74 100644 --- a/lib/src/widgets/buttons/ghost_button.dart +++ b/lib/src/widgets/buttons/ghost_button.dart @@ -15,7 +15,7 @@ class MoonGhostButton extends StatelessWidget { /// The size of the button. final MoonButtonSize? buttonSize; - /// The focus node for the button. + /// {@macro flutter.widgets.Focus.focusNode}. final FocusNode? focusNode; /// The semantic label for the button. @@ -36,7 +36,7 @@ class MoonGhostButton extends StatelessWidget { /// Whether this button should ensure that it has a minimal touch target size. final bool ensureMinimalTouchTargetSize; - /// Whether this button should automatically be focused when it is mounted. + /// {@macro flutter.widgets.Focus.autofocus} final bool autofocus; /// Whether this button should be focusable. diff --git a/lib/src/widgets/buttons/primary_button.dart b/lib/src/widgets/buttons/primary_button.dart index 2f7fae72..4e453c18 100644 --- a/lib/src/widgets/buttons/primary_button.dart +++ b/lib/src/widgets/buttons/primary_button.dart @@ -14,7 +14,7 @@ class MoonPrimaryButton extends StatelessWidget { /// The size of the button. final MoonButtonSize? buttonSize; - /// The focus node for the button. + /// {@macro flutter.widgets.Focus.focusNode}. final FocusNode? focusNode; /// The semantic label for the button. @@ -35,7 +35,7 @@ class MoonPrimaryButton extends StatelessWidget { /// Whether this button should ensure that it has a minimal touch target size. final bool ensureMinimalTouchTargetSize; - /// Whether this button should automatically be focused when it is mounted. + /// {@macro flutter.widgets.Focus.autofocus} final bool autofocus; /// Whether this button should be focusable. diff --git a/lib/src/widgets/buttons/secondary_button.dart b/lib/src/widgets/buttons/secondary_button.dart index c782bb35..19b4ff1a 100644 --- a/lib/src/widgets/buttons/secondary_button.dart +++ b/lib/src/widgets/buttons/secondary_button.dart @@ -12,7 +12,7 @@ class MoonSecondaryButton extends StatelessWidget { /// The size of the button. final MoonButtonSize? buttonSize; - /// The focus node for the button. + /// {@macro flutter.widgets.Focus.focusNode}. final FocusNode? focusNode; /// The semantic label for the button. @@ -33,7 +33,7 @@ class MoonSecondaryButton extends StatelessWidget { /// Whether this button should ensure that it has a minimal touch target size. final bool ensureMinimalTouchTargetSize; - /// Whether this button should automatically be focused when it is mounted. + /// {@macro flutter.widgets.Focus.autofocus} final bool autofocus; /// Whether this button should be focusable. diff --git a/lib/src/widgets/buttons/tertiary_button.dart b/lib/src/widgets/buttons/tertiary_button.dart index 0f13c359..3d16313a 100644 --- a/lib/src/widgets/buttons/tertiary_button.dart +++ b/lib/src/widgets/buttons/tertiary_button.dart @@ -14,7 +14,7 @@ class MoonTertiaryButton extends StatelessWidget { /// The size of the button. final MoonButtonSize? buttonSize; - /// The focus node for the button. + /// {@macro flutter.widgets.Focus.focusNode}. final FocusNode? focusNode; /// The semantic label for the button. @@ -35,7 +35,7 @@ class MoonTertiaryButton extends StatelessWidget { /// Whether this button should ensure that it has a minimal touch target size. final bool ensureMinimalTouchTargetSize; - /// Whether this button should automatically be focused when it is mounted. + /// {@macro flutter.widgets.Focus.autofocus} final bool autofocus; /// Whether this button should be focusable. diff --git a/lib/src/widgets/checkbox/checkbox.dart b/lib/src/widgets/checkbox/checkbox.dart index 02fd5602..ea4deed0 100644 --- a/lib/src/widgets/checkbox/checkbox.dart +++ b/lib/src/widgets/checkbox/checkbox.dart @@ -1,9 +1,12 @@ 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/effects/focus_effects.dart'; +import 'package:moon_design/src/utils/touch_target_padding.dart'; +import 'package:moon_design/src/widgets/checkbox/checkbox_painter.dart'; -class MoonCheckbox extends StatelessWidget { +class MoonCheckbox extends StatefulWidget { /// Whether this checkbox is checked. /// /// When [tristate] is true, a value of null corresponds to the mixed state. @@ -25,85 +28,243 @@ class MoonCheckbox extends StatelessWidget { /// If tristate is false (the default), [value] must not be null. final bool tristate; + /// The size of the checkbox's tap target. + /// + /// Defaults to 40. + final double tapAreaSizeValue; + /// The color to use when this checkbox is checked. final Color? activeColor; + /// The color to use when this checkbox is checked. + final Color? borderColor; + /// The color to use for the check icon when this checkbox is checked. final Color? checkColor; /// The color to use for the checkbox's background when the checkbox is not checked. final Color? fillColor; - /// The focus node for the checkbox. + /// {@macro flutter.widgets.Focus.focusNode}. final FocusNode? focusNode; + /// {@macro flutter.widgets.Focus.autofocus} + final bool autofocus; + const MoonCheckbox({ super.key, required this.value, required this.onChanged, this.tristate = false, + this.tapAreaSizeValue = 40, this.activeColor, + this.borderColor, this.checkColor, this.fillColor, this.focusNode, + this.autofocus = false, }); + static Widget withLabel( + BuildContext context, { + Key? key, + required bool? value, + required void Function(bool?)? onChanged, + bool tristate = false, + double tapAreaSizeValue = 40, + Color? activeColor, + Color? borderColor, + Color? checkColor, + Color? fillColor, + 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: [ + MoonCheckbox( + key: key, + value: value, + onChanged: onChanged, + tristate: tristate, + tapAreaSizeValue: 0, + activeColor: activeColor, + borderColor: borderColor, + checkColor: checkColor, + fillColor: fillColor, + focusNode: focusNode, + autofocus: autofocus, + ), + const SizedBox(width: 12), + AnimatedOpacity( + opacity: isInteractive ? 1 : effectiveDisabledOpacityValue, + duration: effectiveFocusEffectDuration, + child: Text( + label, + style: effectiveTextStyle, + ), + ), + ], + ), + ), + ), + ); + } + + @override + State createState() => _MoonCheckboxState(); +} + +class _MoonCheckboxState extends State with TickerProviderStateMixin, ToggleableStateMixin { + final MoonCheckboxPainter _painter = MoonCheckboxPainter(); + bool? _previousValue; + + @override + ValueChanged? get onChanged => widget.onChanged; + + @override + bool get tristate => widget.tristate; + + @override + bool? get value => widget.value; + + BorderSide? _resolveSide(BorderSide? side) { + if (side is MaterialStateBorderSide) { + return MaterialStateProperty.resolveAs(side, states); + } + if (!states.contains(MaterialState.selected)) { + return side; + } + return null; + } + + @override + void initState() { + super.initState(); + _previousValue = widget.value; + } + + @override + void didUpdateWidget(MoonCheckbox oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.value != widget.value) { + _previousValue = oldWidget.value; + animateToValue(); + } + } + + @override + void dispose() { + _painter.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { + const Size size = Size(16, 16); + final Color effectiveActiveColor = - activeColor ?? context.moonTheme?.checkboxTheme.colors.activeColor ?? MoonColors.light.piccolo; + widget.activeColor ?? context.moonTheme?.checkboxTheme.colors.activeColor ?? MoonColors.light.piccolo; final Color effectiveCheckColor = - activeColor ?? context.moonTheme?.checkboxTheme.colors.checkColor ?? MoonColors.light.goten; + widget.checkColor ?? context.moonTheme?.checkboxTheme.colors.checkColor ?? MoonColors.light.goten; final Color effectiveFillColor = - activeColor ?? context.moonTheme?.checkboxTheme.colors.activeColor ?? MoonColors.light.piccolo; + widget.fillColor ?? context.moonTheme?.checkboxTheme.colors.fillColor ?? Colors.transparent; + + final Color effectiveBorderColor = + widget.borderColor ?? context.moonTheme?.checkboxTheme.colors.borderColor ?? MoonColors.light.trunks; + + final double effectiveFocusEffectExtent = + context.moonEffects?.controlFocusEffect.effectExtent ?? MoonFocusEffects.lightFocusEffect.effectExtent; final Color effectiveFocusEffectColor = context.moonEffects?.controlFocusEffect.effectColor ?? MoonFocusEffects.lightFocusEffect.effectColor; - final Color effectiveBorderColor = - activeColor ?? context.moonTheme?.checkboxTheme.colors.borderColor ?? MoonColors.light.trunks; + final Curve effectiveFocusEffectCurve = + context.moonEffects?.controlFocusEffect.effectCurve ?? MoonFocusEffects.lightFocusEffect.effectCurve; - final MaterialStateProperty resolvedFillColor = - MaterialStateProperty.resolveWith((Set states) { - if (states.contains(MaterialState.disabled)) { - return effectiveFillColor.withOpacity(.32); - } + final Duration effectiveFocusEffectDuration = + context.moonEffects?.controlFocusEffect.effectDuration ?? MoonFocusEffects.lightFocusEffect.effectDuration; - if (states.contains(MaterialState.focused)) { - return effectiveFocusEffectColor; - } + final double effectiveBorderRadiusValue = context.moonTheme?.checkboxTheme.properties.borderRadiusValue ?? 4; - return effectiveFillColor; - }); + final double effectiveDisabledOpacityValue = context.moonTheme?.opacity.disabled ?? MoonOpacity.opacities.disabled; - final double effectiveBorderRadiusValue = context.moonTheme?.checkboxTheme.properties.borderRadiusValue ?? 4; + final SmoothBorderRadius resolvedBorderRadius = SmoothBorderRadius.all( + SmoothRadius( + cornerRadius: effectiveBorderRadiusValue, + cornerSmoothing: 1, + ), + ); - return SizedBox( - height: 28, - width: 28, - child: FittedBox( - child: Checkbox( - value: value, - onChanged: onChanged, - tristate: tristate, - focusNode: focusNode, - activeColor: effectiveActiveColor, - checkColor: effectiveCheckColor, - fillColor: resolvedFillColor, - focusColor: effectiveFocusEffectColor, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - side: BorderSide(color: effectiveBorderColor, width: 1.25), - shape: SmoothRectangleBorder( - borderRadius: SmoothBorderRadius.all( - SmoothRadius( - cornerRadius: effectiveBorderRadiusValue, - cornerSmoothing: 1, - ), + final MaterialStateProperty effectiveMouseCursor = + MaterialStateProperty.resolveWith((Set states) { + return MaterialStateMouseCursor.clickable.resolve(states); + }); + + return Semantics( + checked: widget.value ?? false, + mixed: widget.tristate ? widget.value == null : null, + child: TouchTargetPadding( + minSize: Size(widget.tapAreaSizeValue, widget.tapAreaSizeValue), + child: MoonFocusEffect( + show: states.contains(MaterialState.focused), + effectExtent: effectiveFocusEffectExtent, + childBorderRadius: resolvedBorderRadius, + effectColor: effectiveFocusEffectColor, + effectCurve: effectiveFocusEffectCurve, + effectDuration: effectiveFocusEffectDuration, + child: AnimatedOpacity( + opacity: states.contains(MaterialState.disabled) ? effectiveDisabledOpacityValue : 1, + duration: effectiveFocusEffectDuration, + child: buildToggleable( + mouseCursor: effectiveMouseCursor, + focusNode: widget.focusNode, + autofocus: widget.autofocus, + 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 + ..checkColor = effectiveCheckColor + ..value = value + ..previousValue = _previousValue + ..shape = SmoothRectangleBorder(borderRadius: resolvedBorderRadius) + ..side = _resolveSide(BorderSide(color: effectiveBorderColor)), ), ), - splashRadius: 0, ), ), ); diff --git a/lib/src/widgets/checkbox/checkbox_painter.dart b/lib/src/widgets/checkbox/checkbox_painter.dart new file mode 100644 index 00000000..7c4bacc6 --- /dev/null +++ b/lib/src/widgets/checkbox/checkbox_painter.dart @@ -0,0 +1,179 @@ +import 'package:flutter/material.dart'; + +const double _kEdgeSize = 16; +const double _kStrokeWidth = 1.0; + +class MoonCheckboxPainter extends ToggleablePainter { + Color get checkColor => _checkColor!; + Color? _checkColor; + set checkColor(Color value) { + if (_checkColor == value) { + return; + } + _checkColor = value; + notifyListeners(); + } + + bool? get value => _value; + bool? _value; + set value(bool? value) { + if (_value == value) { + return; + } + _value = value; + notifyListeners(); + } + + bool? get previousValue => _previousValue; + bool? _previousValue; + set previousValue(bool? value) { + if (_previousValue == value) { + return; + } + _previousValue = value; + notifyListeners(); + } + + OutlinedBorder get shape => _shape!; + OutlinedBorder? _shape; + set shape(OutlinedBorder value) { + if (_shape == value) { + return; + } + _shape = value; + notifyListeners(); + } + + BorderSide? get side => _side; + BorderSide? _side; + set side(BorderSide? value) { + if (_side == value) { + return; + } + _side = value; + 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); + return rect; + } + + // The checkbox's border color if value == false, or its fill color when + // value == true or null. + Color _colorAt(double t) { + // As t goes from 0.0 to 0.25, animate from the inactiveColor to activeColor. + return t >= 0.25 ? activeColor : Color.lerp(inactiveColor, activeColor, t * 4.0)!; + } + + // White stroke used to paint the check and dash. + Paint _createStrokePaint() { + return Paint() + ..color = checkColor + ..style = PaintingStyle.stroke + ..strokeWidth = _kStrokeWidth; + } + + void _drawBox(Canvas canvas, Rect outer, Paint paint, BorderSide? side, bool fill) { + if (fill) { + canvas.drawPath(shape.getOuterPath(outer), paint); + } + if (side != null) { + shape.copyWith(side: side).paint(canvas, outer); + } + } + + void _drawCheck(Canvas canvas, Offset origin, double t, Paint paint) { + assert(t >= 0.0 && t <= 1.0); + // As t goes from 0.0 to 1.0, animate the two check mark strokes from the + // short side to the long side. + final Path path = Path(); + const Offset start = Offset(_kEdgeSize * 0.15, _kEdgeSize * 0.45); + const Offset mid = Offset(_kEdgeSize * 0.4, _kEdgeSize * 0.7); + const Offset end = Offset(_kEdgeSize * 0.85, _kEdgeSize * 0.25); + if (t < 0.5) { + final double strokeT = t * 2.0; + final Offset drawMid = Offset.lerp(start, mid, strokeT)!; + path.moveTo(origin.dx + start.dx, origin.dy + start.dy); + path.lineTo(origin.dx + drawMid.dx, origin.dy + drawMid.dy); + } else { + final double strokeT = (t - 0.5) * 2.0; + final Offset drawEnd = Offset.lerp(mid, end, strokeT)!; + path.moveTo(origin.dx + start.dx, origin.dy + start.dy); + path.lineTo(origin.dx + mid.dx, origin.dy + mid.dy); + path.lineTo(origin.dx + drawEnd.dx, origin.dy + drawEnd.dy); + } + canvas.drawPath(path, paint); + } + + void _drawDash(Canvas canvas, Offset origin, double t, Paint paint) { + assert(t >= 0.0 && t <= 1.0); + // As t goes from 0.0 to 1.0, animate the horizontal line from the + // mid point outwards. + const Offset start = Offset(_kEdgeSize * 0.2, _kEdgeSize * 0.5); + const Offset mid = Offset(_kEdgeSize * 0.5, _kEdgeSize * 0.5); + const Offset end = Offset(_kEdgeSize * 0.8, _kEdgeSize * 0.5); + final Offset drawStart = Offset.lerp(start, mid, 1.0 - t)!; + final Offset drawEnd = Offset.lerp(mid, end, t)!; + canvas.drawLine(origin + drawStart, origin + drawEnd, paint); + } + + @override + void paint(Canvas canvas, Size size) { + //paintRadialReaction(canvas: canvas, origin: size.center(Offset.zero)); + + final Paint strokePaint = _createStrokePaint(); + final Offset origin = size / 2.0 - const Size.square(_kEdgeSize) / 2.0 as Offset; + final AnimationStatus status = position.status; + final double tNormalized = status == AnimationStatus.forward || status == AnimationStatus.completed + ? position.value + : 1.0 - position.value; + + // Four cases: false to null, false to true, null to false, true to false + if (previousValue == false || value == false) { + final double t = value == false ? 1.0 - tNormalized : tNormalized; + final Rect outer = _outerRectAt(origin, t); + final Paint paint = Paint()..color = _colorAt(t); + + if (t <= 0.5) { + final BorderSide border = side ?? BorderSide(width: 2, color: paint.color); + _drawBox(canvas, outer, paint, border, true); // only paint the border + } else { + _drawBox(canvas, outer, paint, side, true); + final double tShrink = (t - 0.5) * 2.0; + if (previousValue == null || value == null) { + _drawDash(canvas, origin, tShrink, strokePaint); + } else { + _drawCheck(canvas, origin, tShrink, strokePaint); + } + } + } else { + // Two cases: null to true, true to null + final Rect outer = _outerRectAt(origin, 1.0); + final Paint paint = Paint()..color = _colorAt(1.0); + + _drawBox(canvas, outer, paint, side, true); + if (tNormalized <= 0.5) { + final double tShrink = 1.0 - tNormalized * 2.0; + if (previousValue ?? false) { + _drawCheck(canvas, origin, tShrink, strokePaint); + } else { + _drawDash(canvas, origin, tShrink, strokePaint); + } + } else { + final double tExpand = (tNormalized - 0.5) * 2.0; + if (value ?? false) { + _drawCheck(canvas, origin, tExpand, strokePaint); + } else { + _drawDash(canvas, origin, tExpand, strokePaint); + } + } + } + } +} diff --git a/lib/src/widgets/chips/chip.dart b/lib/src/widgets/chips/chip.dart index cc6b0f85..30063810 100644 --- a/lib/src/widgets/chips/chip.dart +++ b/lib/src/widgets/chips/chip.dart @@ -25,7 +25,7 @@ class MoonChip extends StatelessWidget { /// The size of the chip. final MoonChipSize? chipSize; - /// The focus node for the chip. + /// {@macro flutter.widgets.Focus.focusNode}. final FocusNode? focusNode; /// The semantic label for the chip. @@ -55,7 +55,7 @@ class MoonChip extends StatelessWidget { /// The minimum size of the touch target. final double minTouchTargetSize; - /// Whether the chip should automatically be focused when it is mounted. + /// {@macro flutter.widgets.Focus.autofocus} final bool autofocus; /// Whether the chip is active/selected. @@ -121,6 +121,7 @@ class MoonChip extends StatelessWidget { /// The widget in the right icon slot of the chip. final Widget? rightIcon; + /// MDS chip widget const MoonChip({ super.key, this.onTap, diff --git a/lib/src/widgets/chips/ghost_chip.dart b/lib/src/widgets/chips/ghost_chip.dart index 79c480b1..b039e213 100644 --- a/lib/src/widgets/chips/ghost_chip.dart +++ b/lib/src/widgets/chips/ghost_chip.dart @@ -14,7 +14,7 @@ class MoonGhostChip extends StatelessWidget { /// The size of the chip. final MoonChipSize? chipSize; - /// The focus node for the chip. + /// {@macro flutter.widgets.Focus.focusNode}. final FocusNode? focusNode; /// The semantic label for the chip. @@ -44,7 +44,7 @@ class MoonGhostChip extends StatelessWidget { /// The minimum size of the touch target. final double minTouchTargetSize; - /// Whether the chip should automatically be focused when it is mounted. + /// {@macro flutter.widgets.Focus.autofocus} final bool autofocus; /// Whether the chip is active/selected. @@ -107,6 +107,7 @@ class MoonGhostChip extends StatelessWidget { /// The widget in the right icon slot of the chip. final Widget? rightIcon; + /// MDS ghost chip widget. const MoonGhostChip({ super.key, this.onTap, diff --git a/lib/src/widgets/common/base_control.dart b/lib/src/widgets/common/base_control.dart index c63cd3b4..e3a5d6d4 100644 --- a/lib/src/widgets/common/base_control.dart +++ b/lib/src/widgets/common/base_control.dart @@ -25,10 +25,10 @@ class MoonBaseControl extends StatefulWidget { /// The callback that is called when the control is long-pressed. final VoidCallback? onLongPress; - /// The focus node for this control. + /// {@macro flutter.widgets.Focus.focusNode}. final FocusNode? focusNode; - /// Whether this control should autofocus when it is first added to the tree. + /// {@macro flutter.widgets.Focus.autofocus} final bool autofocus; /// Whether this control should be focusable. @@ -144,7 +144,7 @@ class MoonBaseControl extends StatefulWidget { this.pulseEffectCurve, this.scaleEffectCurve, this.borderRadius = BorderRadius.zero, - this.cursor = MouseCursor.defer, + this.cursor = SystemMouseCursors.click, required this.builder, }); @@ -168,7 +168,7 @@ class _MoonBaseControlState extends State { bool get _canAnimatePulse => widget.showPulseEffect && _isEnabled; bool get _canAnimateScale => widget.showScaleAnimation && _isEnabled && (_isPressed || _isLongPressed); - MouseCursor get _cursor => _isEnabled ? widget.cursor : SystemMouseCursors.forbidden; + MouseCursor get _cursor => _isEnabled ? widget.cursor : SystemMouseCursors.basic; FocusNode get _effectiveFocusNode => widget.focusNode ?? (_focusNode ??= FocusNode()); void _handleHover(bool hover) { diff --git a/lib/src/widgets/loaders/circular_loader.dart b/lib/src/widgets/loaders/circular_loader.dart index e9905d29..15d285b6 100644 --- a/lib/src/widgets/loaders/circular_loader.dart +++ b/lib/src/widgets/loaders/circular_loader.dart @@ -14,13 +14,25 @@ enum MoonCircularLoaderSize { } class MoonCircularLoader extends StatelessWidget { + /// Size of the circular loader widget. final MoonCircularLoaderSize? loaderSize; + + /// Size value of the circular loader widget. final double? sizeValue; + + /// Stroke width of the circular loader widget. final double? strokeWidth; + + /// Stroke cap of the circular loader widget. final StrokeCap? strokeCap; + + /// Color of the circular loader widget. final Color? color; + + /// Background color of the circular loader widget. final Color? backgroundColor; + /// MDS circular loader widget. const MoonCircularLoader({ super.key, this.loaderSize, diff --git a/lib/src/widgets/loaders/linear_loader.dart b/lib/src/widgets/loaders/linear_loader.dart index 33e810ef..3758ccd1 100644 --- a/lib/src/widgets/loaders/linear_loader.dart +++ b/lib/src/widgets/loaders/linear_loader.dart @@ -14,12 +14,22 @@ enum MoonLinearLoaderSize { } class MoonLinearLoader extends StatelessWidget { + /// Size of the linear loader widget. final MoonLinearLoaderSize? loaderSize; + + /// Height of the linear loader widget. final double? height; + + /// Color of the linear loader widget. final Color? color; + + /// Background color of the linear loader widget. final Color? backgroundColor; + + /// Border radius value of the linear loader widget. final double? borderRadiusValue; + /// MDS linear loader widget. const MoonLinearLoader({ super.key, this.loaderSize, diff --git a/lib/src/widgets/popover/popover.dart b/lib/src/widgets/popover/popover.dart index eaa80f25..6e6b7993 100644 --- a/lib/src/widgets/popover/popover.dart +++ b/lib/src/widgets/popover/popover.dart @@ -79,6 +79,7 @@ class MoonPopover extends StatefulWidget { /// The [child] widget which the popover will target. final Widget child; + /// MDS popover widget. const MoonPopover({ super.key, required this.show, diff --git a/lib/src/widgets/progress/circular_progress.dart b/lib/src/widgets/progress/circular_progress.dart index 84ead21a..00d719c9 100644 --- a/lib/src/widgets/progress/circular_progress.dart +++ b/lib/src/widgets/progress/circular_progress.dart @@ -14,14 +14,28 @@ enum MoonCircularProgressSize { } class MoonCircularProgress extends StatelessWidget { + /// Size of the circular progress widget. final MoonCircularProgressSize? circularProgressSize; + + /// Value of the circular progress widget. final double value; + + /// Size value of the circular progress widget. final double? sizeValue; + + /// Stroke width of the circular progress widget. final double? strokeWidth; + + /// Color of the circular progress widget. final Color? color; + + /// Background color of the circular progress widget. final Color? backgroundColor; + + /// Stroke cap of the circular progress widget. final StrokeCap? strokeCap; + /// MDS circular progress widget. const MoonCircularProgress({ super.key, this.circularProgressSize, diff --git a/lib/src/widgets/progress/linear_progress.dart b/lib/src/widgets/progress/linear_progress.dart index 8c120214..a468b10c 100644 --- a/lib/src/widgets/progress/linear_progress.dart +++ b/lib/src/widgets/progress/linear_progress.dart @@ -14,13 +14,25 @@ enum MoonLinearProgressSize { } class MoonLinearProgress extends StatelessWidget { + /// Size of the linear progress widget. final MoonLinearProgressSize? progressSize; + + /// Value of the progress widget. final double value; + + /// Height of the progress widget. final double? height; + + /// Color of the progress widget. final Color? color; + + /// Background color of the progress widget. final Color? backgroundColor; + + /// Border radius value of the progress widget. final double? borderRadiusValue; + /// MDS linear progress widget. const MoonLinearProgress({ super.key, this.progressSize, diff --git a/lib/src/widgets/switch/switch.dart b/lib/src/widgets/switch/switch.dart index f51ad9dd..e704447d 100644 --- a/lib/src/widgets/switch/switch.dart +++ b/lib/src/widgets/switch/switch.dart @@ -26,6 +26,9 @@ class MoonSwitch extends StatefulWidget { /// The size of the switch. final MoonSwitchSize? switchSize; + /// {@macro flutter.widgets.Focus.autofocus} + final bool autofocus; + /// Whether the switch has haptic feedback (vibration) when toggled. final bool hasHapticFeedback; @@ -56,7 +59,7 @@ class MoonSwitch extends StatefulWidget { /// The curve for the switch animation. final Curve? curve; - /// The focus node for the switch. + /// {@macro flutter.widgets.Focus.focusNode}. final FocusNode? focusNode; /// The widget to display when the switch is on (left slot). @@ -71,11 +74,13 @@ class MoonSwitch extends StatefulWidget { /// The widget inside the thumb when the switch is off. final Widget? inactiveThumbWidget; + /// MDS switch widget. const MoonSwitch({ super.key, required this.value, this.onChanged, this.switchSize, + this.autofocus = false, this.width, this.height, this.thumbSizeValue, @@ -293,15 +298,18 @@ class _MoonSwitchState extends State with SingleTickerProviderStateM final List effectiveThumbShadow = context.moonTheme?.switchTheme.shadows.thumbShadows ?? MoonShadows.light.sm; - final double effectiveDisabledOpacity = context.moonTheme?.opacity.disabled ?? MoonOpacity.opacities.disabled; + final double effectiveDisabledOpacityValue = context.moonTheme?.opacity.disabled ?? MoonOpacity.opacities.disabled; final Duration effectiveDuration = widget.duration ?? context.moonTheme?.switchTheme.properties.transitionDuration ?? const Duration(milliseconds: 200); - final Curve effectiveCurve = + final Curve effectiveTransitionCurve = widget.curve ?? context.moonTheme?.switchTheme.properties.transitionCurve ?? Curves.easeInOutCubic; + final double effectiveFocusEffectExtent = + context.moonEffects?.controlFocusEffect.effectExtent ?? MoonFocusEffects.lightFocusEffect.effectExtent; + final Color effectiveFocusEffectColor = context.moonEffects?.controlFocusEffect.effectColor ?? MoonFocusEffects.lightFocusEffect.effectColor; @@ -319,12 +327,12 @@ class _MoonSwitchState extends State with SingleTickerProviderStateM _curvedAnimation ??= CurvedAnimation( parent: _animationController!, - curve: effectiveCurve, + curve: effectiveTransitionCurve, ); _curvedAnimationWithOvershoot ??= CurvedAnimation( parent: _animationController!, - curve: effectiveCurve, + curve: effectiveTransitionCurve, ); _alignmentAnimation = AlignmentTween( @@ -377,10 +385,11 @@ class _MoonSwitchState extends State with SingleTickerProviderStateM child: FocusableActionDetector( enabled: _isInteractive, actions: _actions, + autofocus: widget.autofocus, focusNode: _effectiveFocusNode, onFocusChange: _handleFocusChange, onShowFocusHighlight: _handleFocus, - mouseCursor: _isInteractive ? MouseCursor.defer : SystemMouseCursors.forbidden, + mouseCursor: _isInteractive ? SystemMouseCursors.click : SystemMouseCursors.basic, child: GestureDetector( excludeFromSemantics: true, onTap: _handleTap, @@ -399,9 +408,9 @@ class _MoonSwitchState extends State with SingleTickerProviderStateM animation: _animationController!, builder: (context, child) { return AnimatedOpacity( - opacity: _isInteractive ? 1 : effectiveDisabledOpacity, + opacity: _isInteractive ? 1 : effectiveDisabledOpacityValue, duration: effectiveDuration, - curve: effectiveCurve, + curve: effectiveTransitionCurve, child: SizedBox( width: effectiveWidth, height: effectiveHeight, @@ -455,7 +464,7 @@ class _MoonSwitchState extends State with SingleTickerProviderStateM duration: effectiveDuration, child: MoonFocusEffect( show: _isFocused, - effectExtent: effectivePadding.top * 1.5, + effectExtent: effectiveFocusEffectExtent, effectColor: effectiveFocusEffectColor, effectCurve: effectiveFocusEffectCurve, effectDuration: effectiveFocusEffectDuration, diff --git a/lib/src/widgets/tag/tag.dart b/lib/src/widgets/tag/tag.dart index 97043a21..bf5d17db 100644 --- a/lib/src/widgets/tag/tag.dart +++ b/lib/src/widgets/tag/tag.dart @@ -57,6 +57,7 @@ class MoonTag extends StatelessWidget { /// The widget in the right icon slot of the tag. final Widget? rightIcon; + /// MDS tag widget. const MoonTag({ super.key, this.onTap, @@ -131,55 +132,58 @@ class MoonTag extends StatelessWidget { excludeFromSemantics: true, onTap: onTap, onLongPress: onLongPress, - child: Container( - height: effectiveHeight, - padding: correctedPadding, - constraints: BoxConstraints(minWidth: effectiveHeight), - decoration: ShapeDecoration( - color: effectiveBackgroundColor, - shape: SmoothRectangleBorder( - borderRadius: SmoothBorderRadius.only( - topLeft: SmoothRadius( - cornerRadius: effectiveBorderRadius.topLeft.x, - cornerSmoothing: 1, - ), - topRight: SmoothRadius( - cornerRadius: effectiveBorderRadius.topRight.x, - cornerSmoothing: 1, - ), - bottomLeft: SmoothRadius( - cornerRadius: effectiveBorderRadius.bottomLeft.x, - cornerSmoothing: 1, - ), - bottomRight: SmoothRadius( - cornerRadius: effectiveBorderRadius.bottomRight.x, - cornerSmoothing: 1, + child: MouseRegion( + cursor: onTap != null ? SystemMouseCursors.click : SystemMouseCursors.basic, + child: Container( + height: effectiveHeight, + padding: correctedPadding, + constraints: BoxConstraints(minWidth: effectiveHeight), + decoration: ShapeDecoration( + color: effectiveBackgroundColor, + shape: SmoothRectangleBorder( + borderRadius: SmoothBorderRadius.only( + topLeft: SmoothRadius( + cornerRadius: effectiveBorderRadius.topLeft.x, + cornerSmoothing: 1, + ), + topRight: SmoothRadius( + cornerRadius: effectiveBorderRadius.topRight.x, + cornerSmoothing: 1, + ), + bottomLeft: SmoothRadius( + cornerRadius: effectiveBorderRadius.bottomLeft.x, + cornerSmoothing: 1, + ), + bottomRight: SmoothRadius( + cornerRadius: effectiveBorderRadius.bottomRight.x, + cornerSmoothing: 1, + ), ), ), ), - ), - child: IconTheme( - data: IconThemeData(color: effectiveTextColor, size: effectiveMoonTagSize.iconSizeValue), - child: DefaultTextStyle.merge( - style: isUpperCase - ? effectiveMoonTagSize.upperCaseTextStyle.copyWith(color: effectiveTextColor) - : effectiveMoonTagSize.textStyle.copyWith(color: effectiveTextColor), - child: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (leftIcon != null) - Padding( - padding: EdgeInsets.symmetric(horizontal: effectiveGap), - child: leftIcon, - ), - if (label != null) label!, - if (rightIcon != null) - Padding( - padding: EdgeInsets.symmetric(horizontal: effectiveGap), - child: rightIcon, - ), - ], + child: IconTheme( + data: IconThemeData(color: effectiveTextColor, size: effectiveMoonTagSize.iconSizeValue), + child: DefaultTextStyle.merge( + style: isUpperCase + ? effectiveMoonTagSize.upperCaseTextStyle.copyWith(color: effectiveTextColor) + : effectiveMoonTagSize.textStyle.copyWith(color: effectiveTextColor), + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (leftIcon != null) + Padding( + padding: EdgeInsets.symmetric(horizontal: effectiveGap), + child: leftIcon, + ), + if (label != null) label!, + if (rightIcon != null) + Padding( + padding: EdgeInsets.symmetric(horizontal: effectiveGap), + child: rightIcon, + ), + ], + ), ), ), ), diff --git a/lib/src/widgets/tooltip/tooltip.dart b/lib/src/widgets/tooltip/tooltip.dart index f58484ab..1b0e4ac4 100644 --- a/lib/src/widgets/tooltip/tooltip.dart +++ b/lib/src/widgets/tooltip/tooltip.dart @@ -102,6 +102,7 @@ class MoonTooltip extends StatefulWidget { /// The [child] widget which the tooltip will target. final Widget child; + /// MDS tooltip widget. const MoonTooltip({ super.key, this.onTooltipTap, From 980d4e77b04e7c72b142a5258a46cda7cf5a5157 Mon Sep 17 00:00:00 2001 From: Kypsis Date: Thu, 16 Mar 2023 00:19:30 +0200 Subject: [PATCH 5/5] Revert default story back to Avatar --- example/lib/src/storybook/storybook.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/lib/src/storybook/storybook.dart b/example/lib/src/storybook/storybook.dart index 8379fc0d..507cfc9a 100644 --- a/example/lib/src/storybook/storybook.dart +++ b/example/lib/src/storybook/storybook.dart @@ -34,7 +34,7 @@ class StorybookPage extends StatelessWidget { return Stack( children: [ Storybook( - initialStory: "Checkbox", + initialStory: "Avatar", plugins: _plugins, wrapperBuilder: (context, child) => MaterialApp( title: "Moon Design for Flutter",