From 57bf4cba1d41adc2d463a5f54b323c13645a94e0 Mon Sep 17 00:00:00 2001 From: Harry Sild <46851868+Kypsis@users.noreply.github.com> Date: Tue, 7 Feb 2023 02:10:00 +0200 Subject: [PATCH] [MDS-351] Create Buttons (#5) * [MDS-351] Create MoonControlWrapper * [MDS-351] FilledButton progress commit * [MDS-351] Add MoonPlaceholderIcon * [MDS-351] FilledButton and storybook changes * [MDS-351] Add WidgetSurveyor * [MDS-351] Add more getters to theme extension * [MDS-351] Create MoonOpacity * [MDS-351] Refactor MoonControlWrapper * [MDS-351] Refactor MoonBorders types * [MDS-351] Create MoonButtons theming * [MDS-351] Correct theming lerp logic * [MDS-351] Add button sizings * [MDS-351] Add internal padding logic to buttons * [MDS-351] Theming fixes * [MDS-351] More theming fixes * [MDS-351] Theming doc comments and values fixes * [MDS-351] Refactor theming * [MDS-351] MoonFilledButton progress commit * [MDS-351] Finalise base button api * [MDS-351] Create outlined and text button variants * [MDS-351] Finalise MoonButton effects * [MDS-351] Add SizedScaleTransition * [MDS-351] Pulse effect start condition fix * [MDS-351] Flutter upgrade dependency bump * [MDS-351] Reorganize effects and button * [MDS-351] Add and apply figma_squircle package * [MDS-351] Finalize creating all buttons * [MDS-351] Refactor BaseControl and Button * [MDS-351] Flesh out Buttons story more * [MDS-351] Finalize the Buttons * [MDS-351] Add doc comments --- .vscode/launch.json | 45 ++ analysis_options.yaml | 1 + example/lib/main.dart | 73 +-- example/lib/src/storybook/common/options.dart | 45 ++ .../common/widgets/text_divider.dart | 29 + .../src/storybook/common/widgets/version.dart | 35 ++ example/lib/src/storybook/stories/button.dart | 205 +++++++ example/lib/src/storybook/storybook.dart | 67 +++ example/pubspec.lock | 127 +++-- example/pubspec.yaml | 2 +- lib/moon_design.dart | 37 +- lib/src/theme/borders.dart | 119 ++-- lib/src/theme/button_sizes.dart | 116 ++++ lib/src/theme/button_theme.dart | 80 +++ lib/src/theme/colors.dart | 24 +- lib/src/theme/effects/controls_effects.dart | 86 +++ lib/src/theme/effects/effects.dart | 80 +++ lib/src/theme/effects/focus_effects.dart | 78 +++ lib/src/theme/effects/hover_effects.dart | 78 +++ lib/src/theme/hover_effects.dart | 57 -- lib/src/theme/opacity.dart | 42 ++ lib/src/theme/shadows.dart | 4 +- lib/src/theme/sizes.dart | 57 +- lib/src/theme/text_styles.dart | 4 +- lib/src/theme/theme.dart | 94 ++-- lib/src/theme/transition_effects.dart | 55 -- lib/src/theme/transitions.dart | 47 -- lib/src/theme/typography.dart | 18 +- lib/src/utils/extensions.dart | 10 + lib/src/utils/max_border_radius.dart | 16 + lib/src/utils/measure_size.dart | 49 ++ lib/src/utils/moon_icon.dart | 144 +++++ lib/src/utils/placeholder_icon.dart | 200 +++++++ lib/src/utils/touch_target_padding.dart | 117 ++++ lib/src/utils/widget_surveyor.dart | 154 ++++++ lib/src/widgets/base_control.dart | 517 ++++++++++++++++++ lib/src/widgets/buttons/button.dart | 324 +++++++++++ lib/src/widgets/buttons/ghost_button.dart | 113 ++++ lib/src/widgets/buttons/primary_button.dart | 107 ++++ lib/src/widgets/buttons/secondary_button.dart | 103 ++++ lib/src/widgets/buttons/tertiary_button.dart | 109 ++++ lib/src/widgets/effects/focus_effect.dart | 95 ++++ .../painters/focus_effect_painter.dart | 54 ++ .../painters/pulse_effect_painter.dart | 71 +++ lib/src/widgets/effects/pulse_effect.dart | 134 +++++ pubspec.yaml | 1 + 46 files changed, 3630 insertions(+), 393 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 example/lib/src/storybook/common/options.dart create mode 100644 example/lib/src/storybook/common/widgets/text_divider.dart create mode 100644 example/lib/src/storybook/common/widgets/version.dart create mode 100644 example/lib/src/storybook/stories/button.dart create mode 100644 example/lib/src/storybook/storybook.dart create mode 100644 lib/src/theme/button_sizes.dart create mode 100644 lib/src/theme/button_theme.dart create mode 100644 lib/src/theme/effects/controls_effects.dart create mode 100644 lib/src/theme/effects/effects.dart create mode 100644 lib/src/theme/effects/focus_effects.dart create mode 100644 lib/src/theme/effects/hover_effects.dart delete mode 100644 lib/src/theme/hover_effects.dart create mode 100644 lib/src/theme/opacity.dart delete mode 100644 lib/src/theme/transition_effects.dart delete mode 100644 lib/src/theme/transitions.dart create mode 100644 lib/src/utils/extensions.dart create mode 100644 lib/src/utils/max_border_radius.dart create mode 100644 lib/src/utils/measure_size.dart create mode 100644 lib/src/utils/moon_icon.dart create mode 100644 lib/src/utils/placeholder_icon.dart create mode 100644 lib/src/utils/touch_target_padding.dart create mode 100644 lib/src/utils/widget_surveyor.dart create mode 100644 lib/src/widgets/base_control.dart create mode 100644 lib/src/widgets/buttons/button.dart create mode 100644 lib/src/widgets/buttons/ghost_button.dart create mode 100644 lib/src/widgets/buttons/primary_button.dart create mode 100644 lib/src/widgets/buttons/secondary_button.dart create mode 100644 lib/src/widgets/buttons/tertiary_button.dart create mode 100644 lib/src/widgets/effects/focus_effect.dart create mode 100644 lib/src/widgets/effects/painters/focus_effect_painter.dart create mode 100644 lib/src/widgets/effects/painters/pulse_effect_painter.dart create mode 100644 lib/src/widgets/effects/pulse_effect.dart diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..2aefbdf2 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,45 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "moon_flutter", + "request": "launch", + "type": "dart" + }, + { + "name": "moon_flutter (profile mode)", + "request": "launch", + "type": "dart", + "flutterMode": "profile" + }, + { + "name": "moon_flutter (release mode)", + "request": "launch", + "type": "dart", + "flutterMode": "release" + }, + { + "name": "example", + "cwd": "example", + "request": "launch", + "type": "dart" + }, + { + "name": "example (profile mode)", + "cwd": "example", + "request": "launch", + "type": "dart", + "flutterMode": "profile" + }, + { + "name": "example (release mode)", + "cwd": "example", + "request": "launch", + "type": "dart", + "flutterMode": "release" + } + ] +} \ No newline at end of file diff --git a/analysis_options.yaml b/analysis_options.yaml index 6cdb10b1..2467efb6 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -22,3 +22,4 @@ analyzer: linter: rules: depend_on_referenced_packages: false + use_setters_to_change_properties: false diff --git a/example/lib/main.dart b/example/lib/main.dart index 263ea809..140fb9e1 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,3 +1,4 @@ +import 'package:example/src/storybook/storybook.dart'; import 'package:flutter/material.dart'; import 'package:moon_design/moon_design.dart'; import 'package:storybook_flutter/storybook_flutter.dart'; @@ -35,7 +36,7 @@ class _HomePageState extends State { ), ); - bool _isInStorybookMode = false; + bool _isInStorybookMode = true; void toggleStorybookMode() { setState(() { @@ -46,53 +47,35 @@ class _HomePageState extends State { @override Widget build(BuildContext context) { return _isInStorybookMode - ? Storybook( - initialStory: "Default", - plugins: plugins, - wrapperBuilder: (context, child) => MaterialApp( - theme: ThemeData.light().copyWith(extensions: >[MoonTheme.light]), - darkTheme: ThemeData.dark().copyWith(extensions: >[MoonTheme.dark]), - useInheritedMediaQuery: true, - home: Scaffold(body: child), - ), - stories: [ - Story( - name: "Test Story", - description: "Test description", - builder: (context) { - final theme = Theme.of(context).extension()!; - return Container( - color: theme.colors.piccolo, - alignment: Alignment.center, - child: Text("Test text", style: theme.typography.heading.text32), - ); - }, - ), - ], - ) - : Scaffold( - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - "Moon Design for Flutter", - style: TextStyle( - fontSize: MediaQuery.of(context).size.width > 800 ? 72 : 32, - ), - ), - SizedBox(height: MediaQuery.of(context).size.width > 800 ? 36 : 16), - GestureDetector( - onLongPress: toggleStorybookMode, - child: Text( - "Coming soon...", + ? const StorybookPage() + : MaterialApp( + theme: ThemeData.light().copyWith(extensions: >[MoonTheme.light]), + darkTheme: ThemeData.dark().copyWith(extensions: >[MoonTheme.dark]), + useInheritedMediaQuery: true, + home: Scaffold( + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Moon Design for Flutter", style: TextStyle( - fontSize: MediaQuery.of(context).size.width > 800 ? 24 : 20, - color: const Color(0xFF999CA0), + fontSize: MediaQuery.of(context).size.width > 800 ? 72 : 32, + ), + ), + SizedBox(height: MediaQuery.of(context).size.width > 800 ? 36 : 16), + GestureDetector( + onLongPress: toggleStorybookMode, + child: Text( + "Coming soon...", + style: TextStyle( + fontSize: MediaQuery.of(context).size.width > 800 ? 24 : 20, + color: const Color(0xFF999CA0), + ), ), ), - ), - ], + ], + ), ), ), ); diff --git a/example/lib/src/storybook/common/options.dart b/example/lib/src/storybook/common/options.dart new file mode 100644 index 00000000..dcd9ea01 --- /dev/null +++ b/example/lib/src/storybook/common/options.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:moon_design/moon_design.dart'; +import 'package:storybook_flutter/storybook_flutter.dart'; + +List> colorOptions(BuildContext context) => [ + Option(label: "piccolo", value: context.moonColors!.piccolo), + Option(label: "hit", value: context.moonColors!.hit), + Option(label: "beerus", value: context.moonColors!.beerus), + Option(label: "goku", value: context.moonColors!.goku), + Option(label: "gohan", value: context.moonColors!.gohan), + Option(label: "bulma", value: context.moonColors!.bulma), + Option(label: "trunks", value: context.moonColors!.trunks), + Option(label: "goten", value: context.moonColors!.goten), + Option(label: "popo", value: context.moonColors!.popo), + Option(label: "jiren", value: context.moonColors!.jiren), + Option(label: "heles", value: context.moonColors!.heles), + Option(label: "zeno", value: context.moonColors!.zeno), + Option(label: "krillin100", value: context.moonColors!.krillin100), + Option(label: "krillin60", value: context.moonColors!.krillin60), + Option(label: "krillin10", value: context.moonColors!.krillin10), + Option(label: "chichi100", value: context.moonColors!.chiChi100), + Option(label: "chichi60", value: context.moonColors!.chiChi60), + Option(label: "chichi10", value: context.moonColors!.chiChi10), + Option(label: "roshi100", value: context.moonColors!.roshi60), + Option(label: "roshi60", value: context.moonColors!.roshi100), + Option(label: "roshi10", value: context.moonColors!.roshi10), + Option(label: "frieza100", value: context.moonColors!.frieza100), + Option(label: "frieza60", value: context.moonColors!.frieza60), + Option(label: "frieza10", value: context.moonColors!.frieza10), + Option(label: "dodoria100", value: context.moonColors!.dodoria100), + Option(label: "dodoria60", value: context.moonColors!.dodoria60), + Option(label: "dodoria10", value: context.moonColors!.dodoria10), + Option(label: "cell100", value: context.moonColors!.cell100), + Option(label: "cell60", value: context.moonColors!.cell60), + Option(label: "cell10", value: context.moonColors!.cell10), + Option(label: "raditz100", value: context.moonColors!.raditz100), + Option(label: "raditz60", value: context.moonColors!.raditz60), + Option(label: "raditz10", value: context.moonColors!.raditz10), + Option(label: "nappa100", value: context.moonColors!.nappa100), + Option(label: "nappa60", value: context.moonColors!.nappa60), + Option(label: "nappa10", value: context.moonColors!.nappa10), + Option(label: "whis100", value: context.moonColors!.whis100), + Option(label: "whis60", value: context.moonColors!.whis60), + Option(label: "whis10", value: context.moonColors!.whis10), + ]; diff --git a/example/lib/src/storybook/common/widgets/text_divider.dart b/example/lib/src/storybook/common/widgets/text_divider.dart new file mode 100644 index 00000000..59f49629 --- /dev/null +++ b/example/lib/src/storybook/common/widgets/text_divider.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; + +import 'package:moon_design/moon_design.dart'; + +class TextDivider extends StatelessWidget { + final String text; + + const TextDivider({ + super.key, + required this.text, + }); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + const Expanded(child: Divider()), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Text( + text, + style: context.moonTypography!.text.text12.copyWith(color: context.moonColors!.trunks), + ), + ), + const Expanded(child: Divider()), + ], + ); + } +} diff --git a/example/lib/src/storybook/common/widgets/version.dart b/example/lib/src/storybook/common/widgets/version.dart new file mode 100644 index 00000000..1bb5da4b --- /dev/null +++ b/example/lib/src/storybook/common/widgets/version.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; + +import 'package:moon_design/moon_design.dart'; + +class MoonVersionWidget extends StatelessWidget { + final String version; + + const MoonVersionWidget({ + super.key, + required this.version, + }); + + @override + Widget build(BuildContext context) { + return Material( + child: Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const MoonBrandIcon(), + const SizedBox(width: 8.0), + Text( + "Moon Design", + style: MoonTypography.textStyles.text.text16, + ), + const SizedBox(width: 6.0), + Text("v$version", style: MoonTypography.textStyles.heading.text16), + ], + ), + ), + ); + } +} diff --git a/example/lib/src/storybook/stories/button.dart b/example/lib/src/storybook/stories/button.dart new file mode 100644 index 00000000..8b7e8165 --- /dev/null +++ b/example/lib/src/storybook/stories/button.dart @@ -0,0 +1,205 @@ +import 'package:example/src/storybook/common/options.dart'; +import 'package:example/src/storybook/common/widgets/text_divider.dart'; +import 'package:flutter/material.dart'; +import 'package:moon_design/moon_design.dart'; +import 'package:storybook_flutter/storybook_flutter.dart'; + +class ButtonStory extends Story { + ButtonStory() + : super( + name: "Buttons", + builder: (context) { + final customLabelTextKnob = context.knobs.text( + label: "Custom label text", + initial: "MoonButton", + ); + + final colorsKnob = context.knobs.options( + label: "backgroundColor", + description: "MoonColors variants for base MoonButton.", + initial: context.moonColors!.bulma, + options: colorOptions(context), + ); + + final showBorderKnob = context.knobs.boolean( + label: "showBorder", + description: "Show border for base MoonButton.", + initial: true, + ); + + final showBorderRadiusKnob = context.knobs.sliderInt( + max: 28, + initial: 8, + label: "borderRadius", + description: "Border radius for base MoonButton.", + ); + + final buttonSizesKnob = context.knobs.options( + label: "buttonSize", + description: "Button size variants.", + initial: ButtonSize.md, + options: const [ + Option(label: "xs", value: ButtonSize.xs), + Option(label: "sm", value: ButtonSize.sm), + Option(label: "md", value: ButtonSize.md), + Option(label: "lg", value: ButtonSize.lg), + Option(label: "xl", value: ButtonSize.xl) + ], + ); + + final setRtlModeKnob = context.knobs.boolean( + label: "RTL mode", + description: "Switch between LTR and RTL modes.", + ); + + final showLeftIconKnob = context.knobs.boolean( + label: "Show leftIcon", + description: "Show widget in the leftIcon slot.", + initial: true, + ); + + final showLabelKnob = context.knobs.boolean( + label: "Show label", + description: "Show widget in the label slot.", + initial: true, + ); + + final showRightIconKnob = context.knobs.boolean( + label: "Show rightIcon", + description: "Show widget in the rightIcon slot.", + ); + + final showPulseEffectKnob = context.knobs.boolean( + label: "showPulseEffect", + description: "Show pulse animation.", + ); + + final showPulseEffectJiggleKnob = context.knobs.boolean( + label: "showPulseEffectJiggle", + description: "Show jiggling with pulse animation.", + ); + + final showDisabledKnob = context.knobs.boolean( + label: "Disabled", + description: "onTap() or onLongPress() is null.", + ); + + final setFullWidthKnob = context.knobs.boolean( + label: "isFullWidth", + description: "Set button to full width.", + ); + + return Directionality( + textDirection: setRtlModeKnob ? TextDirection.rtl : TextDirection.ltr, + child: Center( + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: 64), + const TextDivider(text: "Base button"), + const SizedBox(height: 32), + MoonButton( + onTap: showDisabledKnob ? null : () {}, + borderRadius: BorderRadius.circular(showBorderRadiusKnob.toDouble()), + showBorder: showBorderKnob, + buttonSize: buttonSizesKnob, + isFullWidth: setFullWidthKnob, + backgroundColor: colorsKnob, + showPulseEffect: showPulseEffectKnob, + showPulseEffectJiggle: showPulseEffectJiggleKnob, + leftIcon: showLeftIconKnob ? const MoonPlaceholderIcon() : null, + label: showLabelKnob ? Text(customLabelTextKnob) : null, + rightIcon: showRightIconKnob ? const MoonPlaceholderIcon() : null, + ), + const SizedBox(height: 40), + const TextDivider(text: "Main buttons"), + const SizedBox(height: 32), + MoonPrimaryButton( + onTap: showDisabledKnob ? null : () {}, + buttonSize: buttonSizesKnob, + isFullWidth: setFullWidthKnob, + showPulseEffect: showPulseEffectKnob, + leftIcon: showLeftIconKnob ? const MoonPlaceholderIcon() : null, + label: showLabelKnob ? const Text("MoonPrimaryButton") : null, + rightIcon: showRightIconKnob ? const MoonPlaceholderIcon() : null, + ), + const SizedBox(height: 32), + MoonSecondaryButton( + onTap: showDisabledKnob ? null : () {}, + buttonSize: buttonSizesKnob, + isFullWidth: setFullWidthKnob, + showPulseEffect: showPulseEffectKnob, + leftIcon: showLeftIconKnob ? const MoonPlaceholderIcon() : null, + label: showLabelKnob ? const Text("MoonSecondaryButton") : null, + rightIcon: showRightIconKnob ? const MoonPlaceholderIcon() : null, + ), + const SizedBox(height: 32), + MoonTertiaryButton( + onTap: showDisabledKnob ? null : () {}, + buttonSize: buttonSizesKnob, + isFullWidth: setFullWidthKnob, + showPulseEffect: showPulseEffectKnob, + leftIcon: showLeftIconKnob ? const MoonPlaceholderIcon() : null, + label: showLabelKnob ? const Text("MoonTertiaryButton") : null, + rightIcon: showRightIconKnob ? const MoonPlaceholderIcon() : null, + ), + const SizedBox(height: 32), + MoonGhostButton( + onTap: showDisabledKnob ? null : () {}, + buttonSize: buttonSizesKnob, + isFullWidth: setFullWidthKnob, + showPulseEffect: showPulseEffectKnob, + leftIcon: showLeftIconKnob ? const MoonPlaceholderIcon() : null, + label: showLabelKnob ? const Text("MoonGhostButton") : null, + rightIcon: showRightIconKnob ? const MoonPlaceholderIcon() : null, + ), + const SizedBox(height: 40), + const TextDivider(text: "Button with non-standard children"), + const SizedBox(height: 32), + MoonButton( + onTap: showDisabledKnob ? null : () {}, + height: 40, + padding: const EdgeInsets.symmetric(horizontal: 8), + borderRadius: BorderRadius.circular(20), + buttonSize: buttonSizesKnob, + isFullWidth: setFullWidthKnob, + backgroundColor: context.moonTheme!.colors.krillin100, + showPulseEffect: showPulseEffectKnob, + showPulseEffectJiggle: showPulseEffectJiggleKnob, + leftIcon: showLeftIconKnob + ? Container( + width: 24, + height: 24, + padding: const EdgeInsets.all(4), + child: const CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : null, + label: showLabelKnob + ? SizedBox( + width: 20, + height: 20, + child: CircleAvatar( + backgroundColor: context.moonTheme!.colors.trunks, + child: const Icon( + Icons.person, + size: 16, + color: Colors.white, + ), + ), + ) + : null, + rightIcon: showRightIconKnob ? const MoonPlaceholderIcon() : null, + ), + const SizedBox(height: 64), + ], + ), + ), + ), + ); + }, + ); +} diff --git a/example/lib/src/storybook/storybook.dart b/example/lib/src/storybook/storybook.dart new file mode 100644 index 00000000..b91b06fd --- /dev/null +++ b/example/lib/src/storybook/storybook.dart @@ -0,0 +1,67 @@ +import 'package:example/src/storybook/common/widgets/version.dart'; +import 'package:example/src/storybook/stories/button.dart'; +import 'package:flutter/material.dart'; + +import 'package:moon_design/moon_design.dart'; +import 'package:storybook_flutter/storybook_flutter.dart'; + +class StorybookPage extends StatelessWidget { + const StorybookPage({super.key}); + + static final _storyPanelFocusNode = FocusNode(); + + static final _plugins = initializePlugins( + contentsSidePanel: true, + knobsSidePanel: true, + initialDeviceFrameData: DeviceFrameData( + device: Devices.ios.iPhone13, + ), + ); + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Storybook( + initialStory: "Buttons", + plugins: _plugins, + wrapperBuilder: (context, child) => MaterialApp( + theme: ThemeData.light().copyWith(extensions: >[MoonTheme.light]), + darkTheme: ThemeData.dark().copyWith(extensions: >[MoonTheme.dark]), + useInheritedMediaQuery: true, + home: Builder( + builder: (context) { + return Focus( + focusNode: _storyPanelFocusNode, + descendantsAreFocusable: true, + child: GestureDetector( + behavior: HitTestBehavior.deferToChild, + onTap: () { + FocusManager.instance.primaryFocus?.unfocus(); + _storyPanelFocusNode.requestFocus(); + }, + child: Scaffold( + extendBody: true, + extendBodyBehindAppBar: true, + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: child, + ), + ), + ), + ); + }, + ), + ), + stories: [ + ButtonStory(), + ], + ), + const Align( + alignment: Alignment.bottomCenter, + child: MoonVersionWidget(version: "0.1.0"), + ), + ], + ); + } +} diff --git a/example/pubspec.lock b/example/pubspec.lock index 1b4b7698..8d457660 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -5,58 +5,74 @@ packages: dependency: transitive description: name: async - url: "https://pub.dartlang.org" + sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 + url: "https://pub.dev" source: hosted - version: "2.9.0" + version: "2.10.0" boolean_selector: dependency: transitive description: name: boolean_selector - url: "https://pub.dartlang.org" + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" characters: dependency: transitive description: name: characters - url: "https://pub.dartlang.org" + sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + url: "https://pub.dev" source: hosted version: "1.2.1" clock: dependency: transitive description: name: clock - url: "https://pub.dartlang.org" + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" source: hosted version: "1.1.1" collection: dependency: transitive description: name: collection - url: "https://pub.dartlang.org" + sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" cupertino_icons: dependency: "direct main" description: name: cupertino_icons - url: "https://pub.dartlang.org" + sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be + url: "https://pub.dev" source: hosted version: "1.0.5" device_frame: dependency: transitive description: name: device_frame - url: "https://pub.dartlang.org" + sha256: afe76182aec178d171953d9b4a50a43c57c7cf3c77d8b09a48bf30c8fa04dd9d + url: "https://pub.dev" source: hosted version: "1.1.0" fake_async: dependency: transitive description: name: fake_async - url: "https://pub.dartlang.org" + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" source: hosted version: "1.3.1" + figma_squircle: + dependency: transitive + description: + name: figma_squircle + sha256: "790b91a9505e90d246f6efe2fa065ff7fffe658c7b44fe9b5b20c7b0ad3818c0" + url: "https://pub.dev" + source: hosted + version: "0.5.3" flutter: dependency: "direct main" description: flutter @@ -66,7 +82,8 @@ packages: dependency: "direct dev" description: name: flutter_lints - url: "https://pub.dartlang.org" + sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c + url: "https://pub.dev" source: hosted version: "2.0.1" flutter_test: @@ -78,49 +95,64 @@ packages: dependency: transitive description: name: freezed_annotation - url: "https://pub.dartlang.org" + sha256: aeac15850ef1b38ee368d4c53ba9a847e900bb2c53a4db3f6881cbb3cb684338 + url: "https://pub.dev" source: hosted version: "2.2.0" + js: + dependency: transitive + description: + name: js + sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + url: "https://pub.dev" + source: hosted + version: "0.6.5" json_annotation: dependency: transitive description: name: json_annotation - url: "https://pub.dartlang.org" + sha256: c33da08e136c3df0190bd5bbe51ae1df4a7d96e7954d1d7249fea2968a72d317 + url: "https://pub.dev" source: hosted version: "4.8.0" lint: dependency: "direct dev" description: name: lint - url: "https://pub.dartlang.org" + sha256: "3e9343b1cededcfb1e8b40d0dbd3592b7a1c6c0121545663a991433390c2bc97" + url: "https://pub.dev" source: hosted version: "2.0.1" lints: dependency: transitive description: name: lints - url: "https://pub.dartlang.org" + sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" + url: "https://pub.dev" source: hosted version: "2.0.1" matcher: dependency: transitive description: name: matcher - url: "https://pub.dartlang.org" + sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72" + url: "https://pub.dev" source: hosted - version: "0.12.12" + version: "0.12.13" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://pub.dartlang.org" + sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + url: "https://pub.dev" source: hosted - version: "0.1.5" + version: "0.2.0" meta: dependency: transitive description: name: meta - url: "https://pub.dartlang.org" + sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + url: "https://pub.dev" source: hosted version: "1.8.0" moon_design: @@ -134,35 +166,40 @@ packages: dependency: transitive description: name: nested - url: "https://pub.dartlang.org" + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" source: hosted version: "1.0.0" path: dependency: transitive description: name: path - url: "https://pub.dartlang.org" + sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b + url: "https://pub.dev" source: hosted version: "1.8.2" pointer_interceptor: dependency: transitive description: name: pointer_interceptor - url: "https://pub.dartlang.org" + sha256: fee6ba42b910637465bc0d367ba27066c6eccfbc3bc0ceb14831915acc600db0 + url: "https://pub.dev" source: hosted version: "0.9.3+3" provider: dependency: transitive description: name: provider - url: "https://pub.dartlang.org" + sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f + url: "https://pub.dev" source: hosted version: "6.0.5" recase: dependency: transitive description: name: recase - url: "https://pub.dartlang.org" + sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213 + url: "https://pub.dev" source: hosted version: "4.1.0" sky_engine: @@ -174,58 +211,66 @@ packages: dependency: transitive description: name: source_span - url: "https://pub.dartlang.org" + sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" stack_trace: dependency: transitive description: name: stack_trace - url: "https://pub.dartlang.org" + sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.11.0" storybook_flutter: dependency: "direct dev" description: name: storybook_flutter - url: "https://pub.dartlang.org" + sha256: "0ef97a82741e12734af3c104ef6c913af1ed808756214d23bff2bd115e547887" + url: "https://pub.dev" source: hosted - version: "0.11.4" + version: "0.12.0" stream_channel: dependency: transitive description: name: stream_channel - url: "https://pub.dartlang.org" + sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.dartlang.org" + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.2.0" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.dartlang.org" + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" source: hosted version: "1.2.1" test_api: dependency: transitive description: name: test_api - url: "https://pub.dartlang.org" + sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206 + url: "https://pub.dev" source: hosted - version: "0.4.12" + version: "0.4.16" vector_math: dependency: transitive description: name: vector_math - url: "https://pub.dartlang.org" + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" sdks: dart: ">=2.18.6 <3.0.0" flutter: ">=2.10.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index f6a67c0f..0d92df04 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -20,7 +20,7 @@ dev_dependencies: flutter_test: sdk: flutter lint: ^2.0.1 - storybook_flutter: ^0.11.4 + storybook_flutter: ^0.12.0 flutter: diff --git a/lib/moon_design.dart b/lib/moon_design.dart index 89c459ed..06d356a7 100644 --- a/lib/moon_design.dart +++ b/lib/moon_design.dart @@ -1,12 +1,29 @@ library moon_design; -export "package:moon_design/src/theme/borders.dart"; -export "package:moon_design/src/theme/colors.dart"; -export "package:moon_design/src/theme/hover_effects.dart"; -export "package:moon_design/src/theme/shadows.dart"; -export "package:moon_design/src/theme/sizes.dart"; -export "package:moon_design/src/theme/text_styles.dart"; -export "package:moon_design/src/theme/theme.dart"; -export "package:moon_design/src/theme/transition_effects.dart"; -export "package:moon_design/src/theme/transitions.dart"; -export "package:moon_design/src/theme/typography.dart"; +export 'package:moon_design/src/theme/borders.dart'; +export 'package:moon_design/src/theme/button_sizes.dart'; +export 'package:moon_design/src/theme/button_theme.dart'; +export 'package:moon_design/src/theme/colors.dart'; +export 'package:moon_design/src/theme/effects/controls_effects.dart'; +export 'package:moon_design/src/theme/effects/effects.dart'; +export 'package:moon_design/src/theme/effects/focus_effects.dart'; +export 'package:moon_design/src/theme/effects/hover_effects.dart'; +export 'package:moon_design/src/theme/opacity.dart'; +export 'package:moon_design/src/theme/shadows.dart'; +export 'package:moon_design/src/theme/sizes.dart'; +export 'package:moon_design/src/theme/text_styles.dart'; +export 'package:moon_design/src/theme/theme.dart'; +export 'package:moon_design/src/theme/typography.dart'; +export 'package:moon_design/src/utils/extensions.dart'; +export 'package:moon_design/src/utils/measure_size.dart'; +export 'package:moon_design/src/utils/moon_icon.dart'; +export 'package:moon_design/src/utils/placeholder_icon.dart'; +export 'package:moon_design/src/utils/widget_surveyor.dart'; +export 'package:moon_design/src/widgets/base_control.dart'; +export 'package:moon_design/src/widgets/buttons/button.dart'; +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/effects/focus_effect.dart'; +export 'package:moon_design/src/widgets/effects/pulse_effect.dart'; diff --git a/lib/src/theme/borders.dart b/lib/src/theme/borders.dart index 9f62b9d3..763163d6 100644 --- a/lib/src/theme/borders.dart +++ b/lib/src/theme/borders.dart @@ -1,71 +1,76 @@ import 'dart:ui'; -import "package:flutter/foundation.dart"; -import "package:flutter/material.dart"; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; @immutable class MoonBorders extends ThemeExtension with DiagnosticableTreeMixin { + static const borders = MoonBorders( + interactiveXs: BorderRadius.all(Radius.circular(4)), + interactiveSm: BorderRadius.all(Radius.circular(8)), + interactiveMd: BorderRadius.all(Radius.circular(12)), + surfaceXs: BorderRadius.all(Radius.circular(4)), + surfaceSm: BorderRadius.all(Radius.circular(8)), + surfaceMd: BorderRadius.all(Radius.circular(12)), + surfaceLg: BorderRadius.all(Radius.circular(16)), + borderWidth: 1, + ); + /// Interactive radius XS. - final Radius iXs; + final BorderRadius interactiveXs; /// Interactive radius SM. - final Radius iSm; + final BorderRadius interactiveSm; /// Interactive radius MD. - final Radius iMd; + final BorderRadius interactiveMd; /// Surface radius XS. - final Radius sXs; + final BorderRadius surfaceXs; /// Surface radius SM. - final Radius sSm; + final BorderRadius surfaceSm; /// Surface radius MD. - final Radius sMd; + final BorderRadius surfaceMd; /// Surface radius LG. - final Radius sLg; + final BorderRadius surfaceLg; /// Default border width. - final double borderDefault; - - /// Interactive border width. - final double borderInteractive; + final double borderWidth; const MoonBorders({ - this.iXs = const Radius.circular(4), - this.iSm = const Radius.circular(8), - this.iMd = const Radius.circular(12), - this.sXs = const Radius.circular(4), - this.sSm = const Radius.circular(8), - this.sMd = const Radius.circular(12), - this.sLg = const Radius.circular(16), - this.borderDefault = 1, - this.borderInteractive = 2, + required this.interactiveXs, + required this.interactiveSm, + required this.interactiveMd, + required this.surfaceXs, + required this.surfaceSm, + required this.surfaceMd, + required this.surfaceLg, + required this.borderWidth, }); @override MoonBorders copyWith({ - Radius? iXs, - Radius? iSm, - Radius? iMd, - Radius? sXs, - Radius? sSm, - Radius? sMd, - Radius? sLg, - double? borderDefault, - double? borderInteractive, + BorderRadius? interactiveXs, + BorderRadius? interactiveSm, + BorderRadius? interactiveMd, + BorderRadius? surfaceXs, + BorderRadius? surfaceSm, + BorderRadius? surfaceMd, + BorderRadius? surfaceLg, + double? borderWidth, }) { return MoonBorders( - iXs: iXs ?? this.iXs, - iSm: iSm ?? this.iSm, - iMd: iMd ?? this.iMd, - sXs: sXs ?? this.sXs, - sSm: sSm ?? this.sSm, - sMd: sMd ?? this.sMd, - sLg: sLg ?? this.sLg, - borderDefault: borderDefault ?? this.borderDefault, - borderInteractive: borderInteractive ?? this.borderInteractive, + interactiveXs: interactiveXs ?? this.interactiveXs, + interactiveSm: interactiveSm ?? this.interactiveSm, + interactiveMd: interactiveMd ?? this.interactiveMd, + surfaceXs: surfaceXs ?? this.surfaceXs, + surfaceSm: surfaceSm ?? this.surfaceSm, + surfaceMd: surfaceMd ?? this.surfaceMd, + surfaceLg: surfaceLg ?? this.surfaceLg, + borderWidth: borderWidth ?? this.borderWidth, ); } @@ -74,15 +79,14 @@ class MoonBorders extends ThemeExtension with DiagnosticableTreeMix if (other is! MoonBorders) return this; return MoonBorders( - iXs: Radius.lerp(iXs, other.iXs, t)!, - iSm: Radius.lerp(iSm, other.iSm, t)!, - iMd: Radius.lerp(iMd, other.iMd, t)!, - sXs: Radius.lerp(sXs, other.sXs, t)!, - sSm: Radius.lerp(sSm, other.sSm, t)!, - sMd: Radius.lerp(sMd, other.sMd, t)!, - sLg: Radius.lerp(sLg, other.sLg, t)!, - borderDefault: lerpDouble(borderDefault, other.borderDefault, t)!, - borderInteractive: lerpDouble(borderInteractive, other.borderInteractive, t)!, + interactiveXs: BorderRadius.lerp(interactiveXs, other.interactiveXs, t)!, + interactiveSm: BorderRadius.lerp(interactiveSm, other.interactiveSm, t)!, + interactiveMd: BorderRadius.lerp(interactiveMd, other.interactiveMd, t)!, + surfaceXs: BorderRadius.lerp(surfaceXs, other.surfaceXs, t)!, + surfaceSm: BorderRadius.lerp(surfaceSm, other.surfaceSm, t)!, + surfaceMd: BorderRadius.lerp(surfaceMd, other.surfaceMd, t)!, + surfaceLg: BorderRadius.lerp(surfaceLg, other.surfaceLg, t)!, + borderWidth: lerpDouble(borderWidth, other.borderWidth, t)!, ); } @@ -91,14 +95,13 @@ class MoonBorders extends ThemeExtension with DiagnosticableTreeMix super.debugFillProperties(properties); properties ..add(DiagnosticsProperty("type", "MoonBorders")) - ..add(DiagnosticsProperty("iXs", iXs)) - ..add(DiagnosticsProperty("iSm", iSm)) - ..add(DiagnosticsProperty("iMd", iMd)) - ..add(DiagnosticsProperty("sXs", sXs)) - ..add(DiagnosticsProperty("sSm", sSm)) - ..add(DiagnosticsProperty("sMd", sMd)) - ..add(DiagnosticsProperty("sLg", sLg)) - ..add(DoubleProperty("border", borderDefault)) - ..add(DoubleProperty("borderInteractive", borderInteractive)); + ..add(DiagnosticsProperty("interactiveXs", interactiveXs)) + ..add(DiagnosticsProperty("interactiveSm", interactiveSm)) + ..add(DiagnosticsProperty("interactiveMd", interactiveMd)) + ..add(DiagnosticsProperty("surfaceXs", surfaceXs)) + ..add(DiagnosticsProperty("surfaceSm", surfaceSm)) + ..add(DiagnosticsProperty("surfaceMd", surfaceMd)) + ..add(DiagnosticsProperty("surfaceLg", surfaceLg)) + ..add(DoubleProperty("borderWidth", borderWidth)); } } diff --git a/lib/src/theme/button_sizes.dart b/lib/src/theme/button_sizes.dart new file mode 100644 index 00000000..1dba99ef --- /dev/null +++ b/lib/src/theme/button_sizes.dart @@ -0,0 +1,116 @@ +import 'dart:ui'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'package:moon_design/src/theme/borders.dart'; +import 'package:moon_design/src/theme/sizes.dart'; +import 'package:moon_design/src/theme/text_styles.dart'; + +@immutable +class MoonButtonSizes extends ThemeExtension with DiagnosticableTreeMixin { + static final xs = MoonButtonSizes( + height: MoonSizes.sizes.xs, + gap: MoonSizes.sizes.x5s, + padding: EdgeInsets.symmetric(horizontal: MoonSizes.sizes.x4s), + borderRadius: MoonBorders.borders.interactiveXs, + textStyle: MoonTextStyles.heading.text12, + ); + + static final sm = MoonButtonSizes( + height: MoonSizes.sizes.sm, + gap: MoonSizes.sizes.x5s, + padding: EdgeInsets.symmetric(horizontal: MoonSizes.sizes.x3s), + borderRadius: MoonBorders.borders.interactiveSm, + textStyle: MoonTextStyles.heading.text14, + ); + + static final md = MoonButtonSizes( + height: MoonSizes.sizes.md, + gap: MoonSizes.sizes.x4s, + padding: EdgeInsets.symmetric(horizontal: MoonSizes.sizes.x2s), + borderRadius: MoonBorders.borders.interactiveSm, + textStyle: MoonTextStyles.heading.text14, + ); + + static final lg = MoonButtonSizes( + height: MoonSizes.sizes.lg, + gap: MoonSizes.sizes.x3s, + padding: EdgeInsets.symmetric(horizontal: MoonSizes.sizes.x2s), + borderRadius: MoonBorders.borders.interactiveSm, + textStyle: MoonTextStyles.heading.text16, + ); + + static final xl = MoonButtonSizes( + height: MoonSizes.sizes.xl, + gap: MoonSizes.sizes.x2s, + padding: EdgeInsets.symmetric(horizontal: MoonSizes.sizes.xs), + borderRadius: MoonBorders.borders.interactiveMd, + textStyle: MoonTextStyles.heading.text16, + ); + + /// Button height. + final double height; + + /// Space between button children. + final double gap; + + /// Padding around button children. + final EdgeInsets padding; + + /// Button border radius. + final BorderRadius borderRadius; + + /// Button text style. + final TextStyle textStyle; + + const MoonButtonSizes({ + required this.height, + required this.gap, + required this.padding, + required this.borderRadius, + required this.textStyle, + }); + + @override + MoonButtonSizes copyWith({ + double? height, + double? gap, + EdgeInsets? padding, + BorderRadius? borderRadius, + TextStyle? textStyle, + }) { + return MoonButtonSizes( + height: height ?? this.height, + gap: gap ?? this.gap, + padding: padding ?? this.padding, + borderRadius: borderRadius ?? this.borderRadius, + textStyle: textStyle ?? this.textStyle, + ); + } + + @override + MoonButtonSizes lerp(ThemeExtension? other, double t) { + if (other is! MoonButtonSizes) return this; + + return MoonButtonSizes( + height: lerpDouble(height, other.height, t)!, + gap: lerpDouble(gap, other.gap, t)!, + padding: EdgeInsets.lerp(padding, other.padding, t)!, + borderRadius: BorderRadius.lerp(borderRadius, other.borderRadius, t)!, + textStyle: TextStyle.lerp(textStyle, other.textStyle, t)!, + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty("type", "MoonButtonSizes")) + ..add(DoubleProperty("height", height)) + ..add(DoubleProperty("gap", gap)) + ..add(DiagnosticsProperty("padding", padding)) + ..add(DiagnosticsProperty("borderRadius", borderRadius)) + ..add(DiagnosticsProperty("textStyle", textStyle)); + } +} diff --git a/lib/src/theme/button_theme.dart b/lib/src/theme/button_theme.dart new file mode 100644 index 00000000..db172b83 --- /dev/null +++ b/lib/src/theme/button_theme.dart @@ -0,0 +1,80 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'package:moon_design/src/theme/button_sizes.dart'; + +@immutable +class MoonButtonTheme extends ThemeExtension with DiagnosticableTreeMixin { + static final sizes = MoonButtonTheme( + xs: MoonButtonSizes.xs, + sm: MoonButtonSizes.sm, + md: MoonButtonSizes.md, + lg: MoonButtonSizes.lg, + xl: MoonButtonSizes.xl, + ); + + /// Extra small button properties. + final MoonButtonSizes xs; + + /// Small button properties. + final MoonButtonSizes sm; + + /// Medium button properties. + final MoonButtonSizes md; + + /// Large button properties. + final MoonButtonSizes lg; + + /// Extra large button properties. + final MoonButtonSizes xl; + + const MoonButtonTheme({ + required this.xs, + required this.sm, + required this.md, + required this.lg, + required this.xl, + }); + + @override + MoonButtonTheme copyWith({ + MoonButtonSizes? xs, + MoonButtonSizes? sm, + MoonButtonSizes? md, + MoonButtonSizes? lg, + MoonButtonSizes? xl, + }) { + return MoonButtonTheme( + xs: xs ?? this.xs, + sm: sm ?? this.sm, + md: md ?? this.md, + lg: lg ?? this.lg, + xl: xl ?? this.xl, + ); + } + + @override + MoonButtonTheme lerp(ThemeExtension? other, double t) { + if (other is! MoonButtonTheme) return this; + + return MoonButtonTheme( + xs: xs.lerp(other.xs, t), + sm: sm.lerp(other.sm, t), + md: md.lerp(other.md, t), + lg: lg.lerp(other.lg, t), + xl: xl.lerp(other.xl, t), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty("type", "MoonButtonTheme")) + ..add(DiagnosticsProperty("xs", xs)) + ..add(DiagnosticsProperty("sm", sm)) + ..add(DiagnosticsProperty("md", md)) + ..add(DiagnosticsProperty("lg", lg)) + ..add(DiagnosticsProperty("xl", xl)); + } +} diff --git a/lib/src/theme/colors.dart b/lib/src/theme/colors.dart index 4668ffe9..07b3b288 100644 --- a/lib/src/theme/colors.dart +++ b/lib/src/theme/colors.dart @@ -1,5 +1,5 @@ -import "package:flutter/foundation.dart"; -import "package:flutter/material.dart"; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; @immutable class MoonColors extends ThemeExtension with DiagnosticableTreeMixin { @@ -89,10 +89,10 @@ class MoonColors extends ThemeExtension with DiagnosticableTreeMixin // Main colors: - /// Accent color. + /// Primary color. final Color piccolo; - /// Accent color. + /// Secondary (accent) color. final Color hit; /// Border and line color. @@ -101,28 +101,28 @@ class MoonColors extends ThemeExtension with DiagnosticableTreeMixin /// Background color. final Color goku; - /// Background color. + /// Surface color. final Color gohan; - /// Text and icon color. + /// Primary body text and icon color. final Color bulma; - /// Text and icon color. + /// Secondary body text and icon color. final Color trunks; - /// Forced color. + /// Primary button text and icon color. final Color goten; - /// Forced color. + /// Secondary button text and icon color. final Color popo; - /// Ghost/disabled color. + /// Secondary hover effect color. final Color jiren; - /// Generic hover effect color. + /// Primary hover effect color. final Color heles; - /// Modal overlay color. + /// Modal overlay (scrim) color. final Color zeno; // Supportive and Semantic colors: diff --git a/lib/src/theme/effects/controls_effects.dart b/lib/src/theme/effects/controls_effects.dart new file mode 100644 index 00000000..89f4063e --- /dev/null +++ b/lib/src/theme/effects/controls_effects.dart @@ -0,0 +1,86 @@ +import 'dart:ui'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:moon_design/src/theme/colors.dart'; + +@immutable +class MoonControlsEffects extends ThemeExtension with DiagnosticableTreeMixin { + static const controlScaleEffect = MoonControlsEffects( + effectCurve: Curves.easeInOutCubic, + effectDuration: Duration(milliseconds: 150), + effectScalar: 0.95, + ); + + static final controlPulseEffect = MoonControlsEffects( + effectCurve: Curves.easeInOutCubic, + effectDuration: const Duration(milliseconds: 1400), + effectColor: MoonColors.light.piccolo, + effectExtent: 24, + ); + + /// Controls effect curve. + final Curve effectCurve; + + /// Controls effect duration. + final Duration effectDuration; + + /// Controls effect color. + final Color? effectColor; + + /// Controls effect width. + final double? effectExtent; + + /// Controls effect final scale. + final double? effectScalar; + + const MoonControlsEffects({ + required this.effectCurve, + required this.effectDuration, + this.effectColor, + this.effectExtent, + this.effectScalar, + }); + + @override + MoonControlsEffects copyWith({ + Curve? effectCurve, + Duration? effectDuration, + Color? effectColor, + double? effectExtent, + double? effectScalar, + }) { + return MoonControlsEffects( + effectCurve: effectCurve ?? this.effectCurve, + effectDuration: effectDuration ?? this.effectDuration, + effectColor: effectColor ?? this.effectColor, + effectExtent: effectExtent ?? this.effectExtent, + effectScalar: effectScalar ?? this.effectScalar, + ); + } + + @override + MoonControlsEffects lerp(ThemeExtension? other, double t) { + if (other is! MoonControlsEffects) return this; + + return MoonControlsEffects( + effectCurve: other.effectCurve, + effectDuration: lerpDuration(effectDuration, other.effectDuration, t), + effectColor: Color.lerp(effectColor, other.effectColor, t), + effectExtent: lerpDouble(effectExtent, other.effectExtent, t), + effectScalar: lerpDouble(effectScalar, other.effectScalar, t), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty("type", "MoonControlsEffects")) + ..add(DiagnosticsProperty("effectCurve", effectCurve)) + ..add(DiagnosticsProperty("effectDuration", effectDuration)) + ..add(ColorProperty("effectColor", effectColor)) + ..add(DoubleProperty("effectExtent", effectExtent)) + ..add(DoubleProperty("transitionLowerBound", effectScalar)); + } +} diff --git a/lib/src/theme/effects/effects.dart b/lib/src/theme/effects/effects.dart new file mode 100644 index 00000000..9a937d3c --- /dev/null +++ b/lib/src/theme/effects/effects.dart @@ -0,0 +1,80 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'package:moon_design/src/theme/effects/controls_effects.dart'; +import 'package:moon_design/src/theme/effects/focus_effects.dart'; +import 'package:moon_design/src/theme/effects/hover_effects.dart'; + +@immutable +class MoonEffects extends ThemeExtension with DiagnosticableTreeMixin { + static final light = MoonEffects( + controlScaleEffect: MoonControlsEffects.controlScaleEffect, + controlPulseEffect: MoonControlsEffects.controlPulseEffect, + controlFocusEffect: MoonFocusEffects.lightFocusEffect, + buttonHoverEffect: MoonHoverEffects.lightButtonHoverEffect, + ); + + static final dark = MoonEffects( + controlScaleEffect: MoonControlsEffects.controlScaleEffect, + controlPulseEffect: MoonControlsEffects.controlPulseEffect, + controlFocusEffect: MoonFocusEffects.darkFocusEffect, + buttonHoverEffect: MoonHoverEffects.darkButtonHoverEffect, + ); + + /// Control widgets scale effect. + final MoonControlsEffects controlScaleEffect; + + /// Control widgets focus effect. + final MoonControlsEffects controlPulseEffect; + + /// Control widgets focus effect. + final MoonFocusEffects controlFocusEffect; + + /// Button hover effect. + final MoonHoverEffects buttonHoverEffect; + + const MoonEffects({ + required this.controlScaleEffect, + required this.controlPulseEffect, + required this.controlFocusEffect, + required this.buttonHoverEffect, + }); + + @override + MoonEffects copyWith({ + MoonControlsEffects? controlScaleEffect, + MoonControlsEffects? controlPulseEffect, + MoonFocusEffects? controlFocusEffect, + MoonHoverEffects? buttonHoverEffect, + }) { + return MoonEffects( + controlScaleEffect: controlScaleEffect ?? this.controlScaleEffect, + controlPulseEffect: controlPulseEffect ?? this.controlPulseEffect, + controlFocusEffect: controlFocusEffect ?? this.controlFocusEffect, + buttonHoverEffect: buttonHoverEffect ?? this.buttonHoverEffect, + ); + } + + @override + MoonEffects lerp(ThemeExtension? other, double t) { + if (other is! MoonEffects) return this; + + return MoonEffects( + controlScaleEffect: controlScaleEffect.lerp(other.controlScaleEffect, t), + controlPulseEffect: controlPulseEffect.lerp(other.controlPulseEffect, t), + controlFocusEffect: controlFocusEffect.lerp(other.controlFocusEffect, t), + buttonHoverEffect: buttonHoverEffect.lerp(other.buttonHoverEffect, t), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty("type", "MoonEffects")) + ..add(DiagnosticsProperty("controlScaleEffect", controlScaleEffect)) + ..add(DiagnosticsProperty("controlPulseEffect", controlPulseEffect)) + ..add(DiagnosticsProperty("controlFocusEffect", controlFocusEffect)) + ..add(DiagnosticsProperty("buttonHoverEffect", buttonHoverEffect)); + } +} diff --git a/lib/src/theme/effects/focus_effects.dart b/lib/src/theme/effects/focus_effects.dart new file mode 100644 index 00000000..02ae1803 --- /dev/null +++ b/lib/src/theme/effects/focus_effects.dart @@ -0,0 +1,78 @@ +import 'dart:ui'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +@immutable +class MoonFocusEffects extends ThemeExtension with DiagnosticableTreeMixin { + static const lightFocusEffect = MoonFocusEffects( + effectColor: Colors.black26, + effectExtent: 4, + effectCurve: Curves.easeInOut, + effectDuration: Duration(milliseconds: 150), + ); + + static const darkFocusEffect = MoonFocusEffects( + effectColor: Colors.white24, + effectExtent: 4, + effectCurve: Curves.easeInOut, + effectDuration: Duration(milliseconds: 150), + ); + + /// Focus effect color. + final Color effectColor; + + /// Focus effect extent. + final double effectExtent; + + /// Focus effect curve. + final Curve effectCurve; + + /// Focus effect duration. + final Duration effectDuration; + + const MoonFocusEffects({ + required this.effectColor, + required this.effectExtent, + required this.effectCurve, + required this.effectDuration, + }); + + @override + MoonFocusEffects copyWith({ + Color? effectColor, + double? effectExtent, + Curve? effectCurve, + Duration? effectDuration, + }) { + return MoonFocusEffects( + effectColor: effectColor ?? this.effectColor, + effectExtent: effectExtent ?? this.effectExtent, + effectCurve: effectCurve ?? this.effectCurve, + effectDuration: effectDuration ?? this.effectDuration, + ); + } + + @override + MoonFocusEffects lerp(ThemeExtension? other, double t) { + if (other is! MoonFocusEffects) return this; + + return MoonFocusEffects( + effectColor: Color.lerp(effectColor, other.effectColor, t)!, + effectExtent: lerpDouble(effectExtent, other.effectExtent, t)!, + effectCurve: other.effectCurve, + effectDuration: lerpDuration(effectDuration, other.effectDuration, t), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty("type", "MoonFocusEffects")) + ..add(ColorProperty("effectColor", effectColor)) + ..add(DoubleProperty("effectExtent", effectExtent)) + ..add(DiagnosticsProperty("effectCurve", effectCurve)) + ..add(DiagnosticsProperty("effectDuration", effectDuration)); + } +} diff --git a/lib/src/theme/effects/hover_effects.dart b/lib/src/theme/effects/hover_effects.dart new file mode 100644 index 00000000..5f49c8fd --- /dev/null +++ b/lib/src/theme/effects/hover_effects.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 MoonHoverEffects extends ThemeExtension with DiagnosticableTreeMixin { + static final lightButtonHoverEffect = MoonHoverEffects( + primaryHoverColor: MoonColors.light.heles, + secondaryHoverColor: MoonColors.light.jiren, + hoverCurve: Curves.easeInOut, + hoverDuration: const Duration(milliseconds: 150), + ); + + static final darkButtonHoverEffect = MoonHoverEffects( + primaryHoverColor: MoonColors.dark.heles, + secondaryHoverColor: MoonColors.dark.jiren, + hoverCurve: Curves.easeInOut, + hoverDuration: const Duration(milliseconds: 150), + ); + + /// Primary hover effect color. + final Color primaryHoverColor; + + /// Secondary hover effect color. + final Color secondaryHoverColor; + + /// Hover effect curve. + final Curve hoverCurve; + + /// Hover effect duration. + final Duration hoverDuration; + + const MoonHoverEffects({ + required this.primaryHoverColor, + required this.secondaryHoverColor, + required this.hoverCurve, + required this.hoverDuration, + }); + + @override + MoonHoverEffects copyWith({ + Color? primaryHoverColor, + Color? secondaryHoverColor, + Curve? hoverCurve, + Duration? hoverDuration, + }) { + return MoonHoverEffects( + primaryHoverColor: primaryHoverColor ?? this.primaryHoverColor, + secondaryHoverColor: secondaryHoverColor ?? this.secondaryHoverColor, + hoverCurve: hoverCurve ?? this.hoverCurve, + hoverDuration: hoverDuration ?? this.hoverDuration, + ); + } + + @override + MoonHoverEffects lerp(ThemeExtension? other, double t) { + if (other is! MoonHoverEffects) return this; + + return MoonHoverEffects( + primaryHoverColor: Color.lerp(primaryHoverColor, other.primaryHoverColor, t)!, + secondaryHoverColor: Color.lerp(secondaryHoverColor, other.secondaryHoverColor, t)!, + hoverCurve: other.hoverCurve, + hoverDuration: lerpDuration(hoverDuration, other.hoverDuration, t), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty("type", "MoonHoverEffects")) + ..add(ColorProperty("primaryHoverColor", primaryHoverColor)) + ..add(ColorProperty("secondaryHoverColor", secondaryHoverColor)) + ..add(DiagnosticsProperty("hoverCurve", hoverCurve)) + ..add(DiagnosticsProperty("hoverDuration", hoverDuration)); + } +} diff --git a/lib/src/theme/hover_effects.dart b/lib/src/theme/hover_effects.dart deleted file mode 100644 index 0aa55bab..00000000 --- a/lib/src/theme/hover_effects.dart +++ /dev/null @@ -1,57 +0,0 @@ -import "package:flutter/foundation.dart"; -import "package:flutter/material.dart"; -import 'package:moon_design/src/theme/colors.dart'; - -@immutable -class MoonHoverEffects extends ThemeExtension with DiagnosticableTreeMixin { - static final light = MoonHoverEffects( - primaryHover: MoonColors.light.heles, - secondaryHover: MoonColors.light.jiren, - ); - - static final dark = MoonHoverEffects( - primaryHover: MoonColors.dark.heles, - secondaryHover: MoonColors.dark.jiren, - ); - - /// Primary hover state color. - final Color primaryHover; - - /// Secondary hover state color. - final Color secondaryHover; - - const MoonHoverEffects({ - required this.primaryHover, - required this.secondaryHover, - }); - - @override - MoonHoverEffects copyWith({ - Color? primaryHover, - Color? secondaryHover, - }) { - return MoonHoverEffects( - primaryHover: primaryHover ?? this.primaryHover, - secondaryHover: secondaryHover ?? this.secondaryHover, - ); - } - - @override - MoonHoverEffects lerp(ThemeExtension? other, double t) { - if (other is! MoonHoverEffects) return this; - - return MoonHoverEffects( - primaryHover: Color.lerp(primaryHover, other.primaryHover, t)!, - secondaryHover: Color.lerp(secondaryHover, other.secondaryHover, t)!, - ); - } - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - ..add(DiagnosticsProperty("type", "MoonHoverEffects")) - ..add(ColorProperty("primaryHover", primaryHover)) - ..add(ColorProperty("secondaryHover", secondaryHover)); - } -} diff --git a/lib/src/theme/opacity.dart b/lib/src/theme/opacity.dart new file mode 100644 index 00000000..49f343cc --- /dev/null +++ b/lib/src/theme/opacity.dart @@ -0,0 +1,42 @@ +import 'dart:ui'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +@immutable +class MoonOpacity extends ThemeExtension with DiagnosticableTreeMixin { + static const opacities = MoonOpacity(disabled: 0.68); + + /// Disabled opacity value. + final double disabled; + + const MoonOpacity({ + required this.disabled, + }); + + @override + MoonOpacity copyWith({ + double? disabled, + }) { + return MoonOpacity( + disabled: disabled ?? this.disabled, + ); + } + + @override + MoonOpacity lerp(ThemeExtension? other, double t) { + if (other is! MoonOpacity) return this; + + return MoonOpacity( + disabled: lerpDouble(disabled, other.disabled, t)!, + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty("type", "MoonOpacity")) + ..add(DoubleProperty("disabled", disabled)); + } +} diff --git a/lib/src/theme/shadows.dart b/lib/src/theme/shadows.dart index 33bcfe44..e4d94b54 100644 --- a/lib/src/theme/shadows.dart +++ b/lib/src/theme/shadows.dart @@ -1,5 +1,5 @@ -import "package:flutter/foundation.dart"; -import "package:flutter/material.dart"; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; @immutable class MoonShadows extends ThemeExtension with DiagnosticableTreeMixin { diff --git a/lib/src/theme/sizes.dart b/lib/src/theme/sizes.dart index f1b7b19c..812bf4cb 100644 --- a/lib/src/theme/sizes.dart +++ b/lib/src/theme/sizes.dart @@ -1,51 +1,64 @@ import 'dart:ui'; -import "package:flutter/foundation.dart"; -import "package:flutter/material.dart"; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; @immutable class MoonSizes extends ThemeExtension with DiagnosticableTreeMixin { - /// (5x) Extra small size, default value 4. + static const sizes = MoonSizes( + x5s: 4, + x4s: 8, + x3s: 12, + x2s: 16, + xs: 24, + sm: 32, + md: 40, + lg: 48, + xl: 56, + x2l: 64, + ); + + /// (5x) Extra small size. final double x5s; - /// (4x) Extra small size, default value 8. + /// (4x) Extra small size. final double x4s; - /// (3x) Extra small size, default value 12. + /// (3x) Extra small size. final double x3s; - /// (2x) Extra small size, default value 16. + /// (2x) Extra small size. final double x2s; - /// Extra small size, default value 24. + /// Extra small size. final double xs; - /// Small size, default value 32. + /// Small size. final double sm; - /// Medium size, default value 40. + /// Medium size. final double md; - /// Large size, default value 48. + /// Large size. final double lg; - /// Extra large size, default value 56. + /// Extra large size. final double xl; - /// (2x) Extra large size, default value 64. + /// (2x) Extra large size. final double x2l; const MoonSizes({ - this.x5s = 4, - this.x4s = 8, - this.x3s = 12, - this.x2s = 16, - this.xs = 24, - this.sm = 32, - this.md = 40, - this.lg = 48, - this.xl = 56, - this.x2l = 64, + required this.x5s, + required this.x4s, + required this.x3s, + required this.x2s, + required this.xs, + required this.sm, + required this.md, + required this.lg, + required this.xl, + required this.x2l, }); @override diff --git a/lib/src/theme/text_styles.dart b/lib/src/theme/text_styles.dart index e51d7812..d589ea18 100644 --- a/lib/src/theme/text_styles.dart +++ b/lib/src/theme/text_styles.dart @@ -1,5 +1,5 @@ -import "package:flutter/foundation.dart"; -import "package:flutter/material.dart"; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; @immutable class MoonTextStyles extends ThemeExtension with DiagnosticableTreeMixin { diff --git a/lib/src/theme/theme.dart b/lib/src/theme/theme.dart index 4f51ce5b..2bd41e9c 100644 --- a/lib/src/theme/theme.dart +++ b/lib/src/theme/theme.dart @@ -1,83 +1,93 @@ -import "package:flutter/foundation.dart"; -import "package:flutter/material.dart"; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + import 'package:moon_design/src/theme/borders.dart'; +import 'package:moon_design/src/theme/button_theme.dart'; import 'package:moon_design/src/theme/colors.dart'; -import 'package:moon_design/src/theme/hover_effects.dart'; +import 'package:moon_design/src/theme/effects/effects.dart'; +import 'package:moon_design/src/theme/opacity.dart'; import 'package:moon_design/src/theme/shadows.dart'; import 'package:moon_design/src/theme/sizes.dart'; -import 'package:moon_design/src/theme/transitions.dart'; import 'package:moon_design/src/theme/typography.dart'; @immutable class MoonTheme extends ThemeExtension with DiagnosticableTreeMixin { static final light = MoonTheme( - borders: const MoonBorders(), + buttonTheme: MoonButtonTheme.sizes, + borders: MoonBorders.borders, colors: MoonColors.light, - hoverEffects: MoonHoverEffects.light, + effects: MoonEffects.light, + opacity: MoonOpacity.opacities, shadows: MoonShadows.light, - sizes: const MoonSizes(), - transitions: const MoonTransitions(), - typography: const MoonTypography(), + sizes: MoonSizes.sizes, + typography: MoonTypography.textStyles, ); static final dark = MoonTheme( - borders: const MoonBorders(), + buttonTheme: MoonButtonTheme.sizes, + borders: MoonBorders.borders, colors: MoonColors.dark, - hoverEffects: MoonHoverEffects.dark, + effects: MoonEffects.dark, + opacity: MoonOpacity.opacities, shadows: MoonShadows.dark, - sizes: const MoonSizes(), - transitions: const MoonTransitions(), - typography: const MoonTypography(), + sizes: MoonSizes.sizes, + typography: MoonTypography.textStyles, ); - /// MDS borders. + /// Moon Design System borders. final MoonBorders borders; - /// MDS colors. + /// Moon Design System buttonTheme theming. + final MoonButtonTheme buttonTheme; + + /// Moon Design System colors. final MoonColors colors; - /// MDS hover effects. - final MoonHoverEffects hoverEffects; + /// Moon Design System effects. + final MoonEffects effects; + + /// Moon Design System opacities. + final MoonOpacity opacity; - /// MDS shadows. + /// Moon Design System shadows. final MoonShadows shadows; - /// MDS sizes. + /// Moon Design System sizes. final MoonSizes sizes; - /// MDS transitions. - final MoonTransitions transitions; - - /// MDS typography. + /// Moon Design System typography. final MoonTypography typography; const MoonTheme({ required this.borders, + required this.buttonTheme, required this.colors, - required this.hoverEffects, + required this.effects, + required this.opacity, required this.shadows, required this.sizes, - required this.transitions, required this.typography, }); @override MoonTheme copyWith({ MoonBorders? borders, + MoonButtonTheme? buttonTheme, MoonColors? colors, - MoonHoverEffects? hoverEffects, + MoonEffects? effects, + MoonOpacity? opacity, MoonShadows? shadows, MoonSizes? sizes, - MoonTransitions? transitions, MoonTypography? typography, }) { return MoonTheme( borders: borders ?? this.borders, + buttonTheme: buttonTheme ?? this.buttonTheme, colors: colors ?? this.colors, - hoverEffects: hoverEffects ?? this.hoverEffects, + effects: effects ?? this.effects, + opacity: opacity ?? this.opacity, shadows: shadows ?? this.shadows, sizes: sizes ?? this.sizes, - transitions: transitions ?? this.transitions, typography: typography ?? this.typography, ); } @@ -88,11 +98,12 @@ class MoonTheme extends ThemeExtension with DiagnosticableTreeMixin { return MoonTheme( borders: borders.lerp(other.borders, t), + buttonTheme: buttonTheme.lerp(other.buttonTheme, t), colors: colors.lerp(other.colors, t), - hoverEffects: hoverEffects.lerp(other.hoverEffects, t), + effects: effects.lerp(other.effects, t), + opacity: opacity.lerp(other.opacity, t), shadows: shadows.lerp(other.shadows, t), sizes: sizes.lerp(other.sizes, t), - transitions: transitions.lerp(other.transitions, t), typography: typography.lerp(other.typography, t), ); } @@ -102,12 +113,25 @@ class MoonTheme extends ThemeExtension with DiagnosticableTreeMixin { super.debugFillProperties(properties); properties ..add(DiagnosticsProperty("type", "MoonTheme")) - ..add(DiagnosticsProperty("moonBorders", borders)) + ..add(DiagnosticsProperty("MoonBorders", borders)) + ..add(DiagnosticsProperty("MoonButtonTheme", buttonTheme)) ..add(DiagnosticsProperty("moonColors", colors)) - ..add(DiagnosticsProperty("moonHoverEffects", hoverEffects)) + ..add(DiagnosticsProperty("MoonEffects", effects)) + ..add(DiagnosticsProperty("moonOpacity", opacity)) ..add(DiagnosticsProperty("moonShadows", shadows)) ..add(DiagnosticsProperty("moonSizes", sizes)) - ..add(DiagnosticsProperty("moonTransitions", transitions)) ..add(DiagnosticsProperty("moonTypography", typography)); } } + +extension MoonThemeX on BuildContext { + MoonTheme? get moonTheme => Theme.of(this).extension(); + MoonBorders? get moonBorders => moonTheme?.borders; + MoonButtonTheme? get moonButtonTheme => moonTheme?.buttonTheme; + MoonColors? get moonColors => moonTheme?.colors; + MoonEffects? get moonEffects => moonTheme?.effects; + MoonOpacity? get moonOpacity => moonTheme?.opacity; + MoonShadows? get moonShadows => moonTheme?.shadows; + MoonSizes? get moonSizes => moonTheme?.sizes; + MoonTypography? get moonTypography => moonTheme?.typography; +} diff --git a/lib/src/theme/transition_effects.dart b/lib/src/theme/transition_effects.dart deleted file mode 100644 index 6345dd7a..00000000 --- a/lib/src/theme/transition_effects.dart +++ /dev/null @@ -1,55 +0,0 @@ -import "package:flutter/foundation.dart"; -import "package:flutter/material.dart"; - -@immutable -class MoonTransitionEffects extends ThemeExtension with DiagnosticableTreeMixin { - static const arrival = MoonTransitionEffects( - transitionCurve: Curves.easeOut, - transitionDuration: Duration(milliseconds: 300), - ); - - static const departure = MoonTransitionEffects( - transitionCurve: Curves.easeIn, - transitionDuration: Duration(milliseconds: 200), - ); - - // Transition effect curve. - final Curve transitionCurve; - // Transition effect duration. - final Duration transitionDuration; - - const MoonTransitionEffects({ - required this.transitionCurve, - required this.transitionDuration, - }); - - @override - MoonTransitionEffects copyWith({ - Curve? transitionCurve, - Duration? transitionDuration, - }) { - return MoonTransitionEffects( - transitionCurve: transitionCurve ?? this.transitionCurve, - transitionDuration: transitionDuration ?? this.transitionDuration, - ); - } - - @override - MoonTransitionEffects lerp(ThemeExtension? other, double t) { - if (other is! MoonTransitionEffects) return this; - - return MoonTransitionEffects( - transitionCurve: other.transitionCurve, - transitionDuration: other.transitionDuration, - ); - } - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - ..add(DiagnosticsProperty("type", "MoonTransitionEffects")) - ..add(DiagnosticsProperty("transitionCurve", transitionCurve)) - ..add(DiagnosticsProperty("transitionDuration", transitionDuration)); - } -} diff --git a/lib/src/theme/transitions.dart b/lib/src/theme/transitions.dart deleted file mode 100644 index 906c8c6b..00000000 --- a/lib/src/theme/transitions.dart +++ /dev/null @@ -1,47 +0,0 @@ -import "package:flutter/foundation.dart"; -import "package:flutter/material.dart"; -import 'package:moon_design/src/theme/transition_effects.dart'; - -@immutable -class MoonTransitions extends ThemeExtension with DiagnosticableTreeMixin { - /// Arrival effect. - final MoonTransitionEffects arrival; - - /// Departure effect. - final MoonTransitionEffects departure; - - const MoonTransitions({ - this.arrival = MoonTransitionEffects.arrival, - this.departure = MoonTransitionEffects.departure, - }); - - @override - MoonTransitions copyWith({ - MoonTransitionEffects? arrival, - MoonTransitionEffects? departure, - }) { - return MoonTransitions( - arrival: arrival ?? this.arrival, - departure: departure ?? this.departure, - ); - } - - @override - MoonTransitions lerp(ThemeExtension? other, double t) { - if (other is! MoonTransitions) return this; - - return MoonTransitions( - arrival: other.arrival, - departure: other.departure, - ); - } - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - ..add(DiagnosticsProperty("type", "MoonTransitions")) - ..add(DiagnosticsProperty("arrival", arrival)) - ..add(DiagnosticsProperty("departure", departure)); - } -} diff --git a/lib/src/theme/typography.dart b/lib/src/theme/typography.dart index 994d0ae9..79cea73c 100644 --- a/lib/src/theme/typography.dart +++ b/lib/src/theme/typography.dart @@ -1,9 +1,15 @@ -import "package:flutter/foundation.dart"; -import "package:flutter/material.dart"; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + import 'package:moon_design/src/theme/text_styles.dart'; @immutable class MoonTypography extends ThemeExtension with DiagnosticableTreeMixin { + static const textStyles = MoonTypography( + text: MoonTextStyles.text, + heading: MoonTextStyles.heading, + ); + /// Styles for text. final MoonTextStyles text; @@ -11,8 +17,8 @@ class MoonTypography extends ThemeExtension with DiagnosticableT final MoonTextStyles heading; const MoonTypography({ - this.text = MoonTextStyles.text, - this.heading = MoonTextStyles.heading, + required this.text, + required this.heading, }); @override @@ -31,8 +37,8 @@ class MoonTypography extends ThemeExtension with DiagnosticableT if (other is! MoonTypography) return this; return MoonTypography( - text: other.text, - heading: other.heading, + text: text.lerp(other.text, t), + heading: heading.lerp(other.heading, t), ); } diff --git a/lib/src/utils/extensions.dart b/lib/src/utils/extensions.dart new file mode 100644 index 00000000..65477542 --- /dev/null +++ b/lib/src/utils/extensions.dart @@ -0,0 +1,10 @@ +import 'dart:ui'; +import 'package:flutter/widgets.dart'; + +extension DarkModeX on BuildContext { + /// Is dark mode currently active. + bool get isDarkMode { + final brightness = MediaQuery.of(this).platformBrightness; + return brightness == Brightness.dark; + } +} diff --git a/lib/src/utils/max_border_radius.dart b/lib/src/utils/max_border_radius.dart new file mode 100644 index 00000000..eb43da8a --- /dev/null +++ b/lib/src/utils/max_border_radius.dart @@ -0,0 +1,16 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; + +double maxBorderRadius(BorderRadius? borderRadius) { + if (borderRadius == null) return 0; + + final maxRadiusValue = [ + max(borderRadius.topLeft.x, borderRadius.topLeft.y), + max(borderRadius.topRight.x, borderRadius.topRight.y), + max(borderRadius.bottomLeft.x, borderRadius.bottomLeft.y), + max(borderRadius.bottomRight.x, borderRadius.bottomRight.y) + ].reduce(max); + + return maxRadiusValue; +} diff --git a/lib/src/utils/measure_size.dart b/lib/src/utils/measure_size.dart new file mode 100644 index 00000000..3de84706 --- /dev/null +++ b/lib/src/utils/measure_size.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +typedef OnWidgetSizeChange = void Function(Size size); + +/// Wrap this widget around your widget to get your widget size updates. +class MeasureSize extends SingleChildRenderObjectWidget { + final OnWidgetSizeChange onChange; + final bool getInitialSize; + + const MeasureSize({ + super.key, + required this.onChange, + required Widget super.child, + this.getInitialSize = false, + }); + + @override + RenderObject createRenderObject(BuildContext context) { + return MeasureSizeRenderObject(onChange: onChange, getInitialSize: getInitialSize); + } +} + +class MeasureSizeRenderObject extends RenderProxyBox { + Size? oldSize; + final OnWidgetSizeChange onChange; + final bool getInitialSize; + + MeasureSizeRenderObject({required this.onChange, this.getInitialSize = false}); + + @override + void performLayout() { + super.performLayout(); + + final newSize = child!.size; + + if (oldSize == newSize) return; + + oldSize = newSize; + + if (getInitialSize) { + onChange(child!.size); + } else { + WidgetsBinding.instance.addPostFrameCallback((_) { + onChange(newSize); + }); + } + } +} diff --git a/lib/src/utils/moon_icon.dart b/lib/src/utils/moon_icon.dart new file mode 100644 index 00000000..fa4bcf5e --- /dev/null +++ b/lib/src/utils/moon_icon.dart @@ -0,0 +1,144 @@ +import 'package:flutter/material.dart'; + +class MoonBrandIcon extends StatelessWidget { + final double width; + final double height; + + const MoonBrandIcon({ + super.key, + this.width = 24, + this.height = 24, + }); + + @override + Widget build(BuildContext context) { + return CustomPaint( + size: Size(width, height), + painter: _MoonBrandIconPainter(), + ); + } +} + +class _MoonBrandIconPainter extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + final Path path_0 = Path(); + path_0.moveTo(size.width * 0.3778988, size.height * 0.6000000); + path_0.cubicTo( + size.width * 0.4251542, + size.height * 0.6000000, + size.width * 0.4634583, + size.height * 0.5608250, + size.width * 0.4634583, + size.height * 0.5125000, + ); + path_0.cubicTo( + size.width * 0.4634583, + size.height * 0.4641750, + size.width * 0.4251542, + size.height * 0.4250000, + size.width * 0.3778988, + size.height * 0.4250000, + ); + path_0.cubicTo( + size.width * 0.3306446, + size.height * 0.4250000, + size.width * 0.2923379, + size.height * 0.4641750, + size.width * 0.2923379, + size.height * 0.5125000, + ); + path_0.cubicTo( + size.width * 0.2923379, + size.height * 0.5608250, + size.width * 0.3306446, + size.height * 0.6000000, + size.width * 0.3778988, + size.height * 0.6000000, + ); + path_0.close(); + path_0.moveTo(size.width * 0.3778988, size.height * 0.7750000); + path_0.cubicTo( + size.width * 0.5196625, + size.height * 0.7750000, + size.width * 0.6345833, + size.height * 0.6574750, + size.width * 0.6345833, + size.height * 0.5125000, + ); + path_0.cubicTo( + size.width * 0.6345833, + size.height * 0.3675254, + size.width * 0.5196625, + size.height * 0.2500000, + size.width * 0.3778988, + size.height * 0.2500000, + ); + path_0.cubicTo( + size.width * 0.2361367, + size.height * 0.2500000, + size.width * 0.1212158, + size.height * 0.3675254, + size.width * 0.1212158, + size.height * 0.5125000, + ); + path_0.cubicTo( + size.width * 0.1212158, + size.height * 0.6574750, + size.width * 0.2361367, + size.height * 0.7750000, + size.width * 0.3778988, + size.height * 0.7750000, + ); + path_0.close(); + + final Paint paint0Fill = Paint()..style = PaintingStyle.fill; + paint0Fill.color = Colors.black.withOpacity(1.0); + canvas.drawPath(path_0, paint0Fill); + + final Path path_1 = Path(); + path_1.moveTo(size.width * 0.8484917, size.height * 0.3375000); + path_1.cubicTo( + size.width * 0.8484917, + size.height * 0.3858250, + size.width * 0.8101833, + size.height * 0.4250000, + size.width * 0.7629292, + size.height * 0.4250000, + ); + path_1.cubicTo( + size.width * 0.7156750, + size.height * 0.4250000, + size.width * 0.6773667, + size.height * 0.3858250, + size.width * 0.6773667, + size.height * 0.3375000, + ); + path_1.cubicTo( + size.width * 0.6773667, + size.height * 0.2891750, + size.width * 0.7156750, + size.height * 0.2500000, + size.width * 0.7629292, + size.height * 0.2500000, + ); + path_1.cubicTo( + size.width * 0.8101833, + size.height * 0.2500000, + size.width * 0.8484917, + size.height * 0.2891750, + size.width * 0.8484917, + size.height * 0.3375000, + ); + path_1.close(); + + final Paint paint1Fill = Paint()..style = PaintingStyle.fill; + paint1Fill.color = Colors.black.withOpacity(1.0); + canvas.drawPath(path_1, paint1Fill); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + return true; + } +} diff --git a/lib/src/utils/placeholder_icon.dart b/lib/src/utils/placeholder_icon.dart new file mode 100644 index 00000000..36dd580c --- /dev/null +++ b/lib/src/utils/placeholder_icon.dart @@ -0,0 +1,200 @@ +import 'package:flutter/material.dart'; + +class MoonPlaceholderIcon extends StatelessWidget { + final double width; + final double height; + final Color? iconColor; + final Color? containerBackgroundColor; + + const MoonPlaceholderIcon({ + super.key, + this.width = 24, + this.height = 24, + this.iconColor, + this.containerBackgroundColor, + }); + + @override + Widget build(BuildContext context) { + final effectiveStrokeColor = iconColor ?? DefaultTextStyle.of(context).style.color ?? Colors.black; + final effectiveSize = (DefaultTextStyle.of(context).style.fontSize ?? 14) <= 12 ? 20 : 24; + + return CustomPaint( + size: Size(effectiveSize.toDouble(), effectiveSize.toDouble()), + painter: _MoonPlaceholderIconPainter( + strokeColor: effectiveStrokeColor, + ), + ); + } +} + +class _MoonPlaceholderIconPainter extends CustomPainter { + final Color? strokeColor; + + const _MoonPlaceholderIconPainter({this.strokeColor}); + + @override + void paint(Canvas canvas, Size size) { + final Path path_0 = Path(); + path_0.moveTo(size.width * 0.7283656, size.height * 0.3401438); + path_0.cubicTo( + size.width * 0.7662031, + size.height * 0.3401438, + size.width * 0.7968750, + size.height * 0.3094716, + size.width * 0.7968750, + size.height * 0.2716347, + ); + path_0.cubicTo( + size.width * 0.7968750, + size.height * 0.2337978, + size.width * 0.7662031, + size.height * 0.2031250, + size.width * 0.7283656, + size.height * 0.2031250, + ); + path_0.cubicTo( + size.width * 0.6905281, + size.height * 0.2031250, + size.width * 0.6598563, + size.height * 0.2337978, + size.width * 0.6598563, + size.height * 0.2716347, + ); + path_0.moveTo(size.width * 0.7283656, size.height * 0.3401438); + path_0.cubicTo( + size.width * 0.6905281, + size.height * 0.3401438, + size.width * 0.6598563, + size.height * 0.3094716, + size.width * 0.6598563, + size.height * 0.2716347, + ); + path_0.moveTo(size.width * 0.7283656, size.height * 0.3401438); + path_0.lineTo(size.width * 0.7283656, size.height * 0.6598563); + path_0.moveTo(size.width * 0.6598563, size.height * 0.2716347); + path_0.lineTo(size.width * 0.3401438, size.height * 0.2716347); + path_0.moveTo(size.width * 0.6598563, size.height * 0.7283656); + path_0.cubicTo( + size.width * 0.6598563, + size.height * 0.7662031, + size.width * 0.6905281, + size.height * 0.7968750, + size.width * 0.7283656, + size.height * 0.7968750, + ); + path_0.cubicTo( + size.width * 0.7662031, + size.height * 0.7968750, + size.width * 0.7968750, + size.height * 0.7662031, + size.width * 0.7968750, + size.height * 0.7283656, + ); + path_0.cubicTo( + size.width * 0.7968750, + size.height * 0.6905281, + size.width * 0.7662031, + size.height * 0.6598563, + size.width * 0.7283656, + size.height * 0.6598563, + ); + path_0.moveTo(size.width * 0.6598563, size.height * 0.7283656); + path_0.cubicTo( + size.width * 0.6598563, + size.height * 0.6905281, + size.width * 0.6905281, + size.height * 0.6598563, + size.width * 0.7283656, + size.height * 0.6598563, + ); + path_0.moveTo(size.width * 0.6598563, size.height * 0.7283656); + path_0.lineTo(size.width * 0.3401438, size.height * 0.7283656); + path_0.moveTo(size.width * 0.3401438, size.height * 0.2716347); + path_0.cubicTo( + size.width * 0.3401438, + size.height * 0.3094716, + size.width * 0.3094716, + size.height * 0.3401438, + size.width * 0.2716347, + size.height * 0.3401438, + ); + path_0.moveTo(size.width * 0.3401438, size.height * 0.2716347); + path_0.cubicTo( + size.width * 0.3401438, + size.height * 0.2337978, + size.width * 0.3094716, + size.height * 0.2031250, + size.width * 0.2716347, + size.height * 0.2031250, + ); + path_0.cubicTo( + size.width * 0.2337978, + size.height * 0.2031250, + size.width * 0.2031250, + size.height * 0.2337978, + size.width * 0.2031250, + size.height * 0.2716347, + ); + path_0.cubicTo( + size.width * 0.2031250, + size.height * 0.3094716, + size.width * 0.2337978, + size.height * 0.3401438, + size.width * 0.2716347, + size.height * 0.3401438, + ); + path_0.moveTo(size.width * 0.2716347, size.height * 0.3401438); + path_0.lineTo(size.width * 0.2716347, size.height * 0.6598563); + path_0.moveTo(size.width * 0.3401438, size.height * 0.7283656); + path_0.cubicTo( + size.width * 0.3401438, + size.height * 0.7662031, + size.width * 0.3094716, + size.height * 0.7968750, + size.width * 0.2716347, + size.height * 0.7968750, + ); + path_0.cubicTo( + size.width * 0.2337978, + size.height * 0.7968750, + size.width * 0.2031250, + size.height * 0.7662031, + size.width * 0.2031250, + size.height * 0.7283656, + ); + path_0.cubicTo( + size.width * 0.2031250, + size.height * 0.6905281, + size.width * 0.2337978, + size.height * 0.6598563, + size.width * 0.2716347, + size.height * 0.6598563, + ); + path_0.moveTo(size.width * 0.3401438, size.height * 0.7283656); + path_0.cubicTo( + size.width * 0.3401438, + size.height * 0.6905281, + size.width * 0.3094716, + size.height * 0.6598563, + size.width * 0.2716347, + size.height * 0.6598563, + ); + + final Paint paint0stroke = Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 1; + paint0stroke.color = strokeColor ?? Colors.black; + paint0stroke.strokeCap = StrokeCap.round; + canvas.drawPath(path_0, paint0stroke); + + final Paint paint0fill = Paint()..style = PaintingStyle.fill; + paint0fill.color = Colors.transparent; + canvas.drawPath(path_0, paint0fill); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + return true; + } +} diff --git a/lib/src/utils/touch_target_padding.dart b/lib/src/utils/touch_target_padding.dart new file mode 100644 index 00000000..1f4431ac --- /dev/null +++ b/lib/src/utils/touch_target_padding.dart @@ -0,0 +1,117 @@ +import 'dart:math' as math; + +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +class TouchTargetPadding extends SingleChildRenderObjectWidget { + final Size minSize; + + const TouchTargetPadding({ + super.child, + required this.minSize, + }); + + @override + RenderObject createRenderObject(BuildContext context) { + return _RenderTouchTargetPadding(minSize); + } + + @override + void updateRenderObject(BuildContext context, covariant _RenderTouchTargetPadding renderObject) { + renderObject.minSize = minSize; + } +} + +class _RenderTouchTargetPadding extends RenderShiftedBox { + Size _minSize; + + _RenderTouchTargetPadding(this._minSize, [RenderBox? child]) : super(child); + + Size get minSize => _minSize; + + set minSize(Size value) { + if (_minSize == value) { + return; + } + _minSize = value; + markNeedsLayout(); + } + + @override + double computeMinIntrinsicWidth(double height) { + if (child != null) { + return math.max(child!.getMinIntrinsicWidth(height), minSize.width); + } + return 0.0; + } + + @override + double computeMinIntrinsicHeight(double width) { + if (child != null) { + return math.max(child!.getMinIntrinsicHeight(width), minSize.height); + } + return 0.0; + } + + @override + double computeMaxIntrinsicWidth(double height) { + if (child != null) { + return math.max(child!.getMaxIntrinsicWidth(height), minSize.width); + } + return 0.0; + } + + @override + double computeMaxIntrinsicHeight(double width) { + if (child != null) { + return math.max(child!.getMaxIntrinsicHeight(width), minSize.height); + } + return 0.0; + } + + Size _computeSize({required BoxConstraints constraints, required ChildLayouter layoutChild}) { + if (child != null) { + final Size childSize = layoutChild(child!, constraints); + final double height = math.max(childSize.width, minSize.width); + final double width = math.max(childSize.height, minSize.height); + return constraints.constrain(Size(height, width)); + } + return Size.zero; + } + + @override + Size computeDryLayout(BoxConstraints constraints) { + return _computeSize( + constraints: constraints, + layoutChild: ChildLayoutHelper.dryLayoutChild, + ); + } + + @override + void performLayout() { + size = _computeSize( + constraints: constraints, + layoutChild: ChildLayoutHelper.layoutChild, + ); + if (child != null) { + final BoxParentData childParentData = child!.parentData! as BoxParentData; + childParentData.offset = Alignment.center.alongOffset(size - child!.size as Offset); + } + } + + @override + bool hitTest(BoxHitTestResult result, {required Offset position}) { + if (super.hitTest(result, position: position)) { + return true; + } + final Offset center = child!.size.center(Offset.zero); + return result.addWithRawTransform( + transform: MatrixUtils.forceToPoint(center), + position: center, + hitTest: (BoxHitTestResult result, Offset position) { + assert(position == center); + return child!.hitTest(result, position: center); + }, + ); + } +} diff --git a/lib/src/utils/widget_surveyor.dart b/lib/src/utils/widget_surveyor.dart new file mode 100644 index 00000000..6076d530 --- /dev/null +++ b/lib/src/utils/widget_surveyor.dart @@ -0,0 +1,154 @@ +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except in +// compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; + +/// Class that allows callers to measure the size of arbitrary widgets when +/// laid out with specific constraints. +/// +/// The widget surveyor creates synthetic widget trees to hold the widgets it +/// measures. This is important because if the widgets (or any widgets in their +/// subtrees) depend on any inherited widgets (e.g. [Directionality]) that they +/// assume exist in their ancestry, those assumptions may hold true when the +/// widget is rendered by the application but prove false when the widget is +/// rendered via the widget surveyor. Due to this, callers are advised to +/// either: +/// +/// 1. pass in widgets that don't depend on inherited widgets, or +/// 1. ensure all inherited widget dependencies exist in the widget tree +/// that's passed to the widget surveyor's measure methods. +class WidgetSurveyor { + const WidgetSurveyor(); + + /// Builds a widget from the specified builder, inserts the widget into a + /// synthetic widget tree, lays out the resulting render tree, and returns + /// the size of the laid-out render tree. + /// + /// The build context that's passed to the `builder` argument will represent + /// the root of the synthetic tree. + /// + /// The `constraints` argument specify the constraints that will be passed + /// to the render tree during layout. If unspecified, the widget will be laid + /// out unconstrained. + Size measureBuilder( + WidgetBuilder builder, { + BoxConstraints constraints = const BoxConstraints(), + }) { + return measureWidget(Builder(builder: builder), constraints: constraints); + } + + /// Inserts the specified widget into a synthetic widget tree, lays out the + /// resulting render tree, and returns the size of the laid-out render tree. + /// + /// The `constraints` argument specify the constraints that will be passed + /// to the render tree during layout. If unspecified, the widget will be laid + /// out unconstrained. + Size measureWidget( + Widget widget, { + BoxConstraints constraints = const BoxConstraints(), + }) { + final SurveyorView rendered = _render(widget, constraints); + assert(rendered.hasSize); + return rendered.size; + } + + double measureDistanceToBaseline( + Widget widget, { + TextBaseline baseline = TextBaseline.alphabetic, + BoxConstraints constraints = const BoxConstraints(), + }) { + final SurveyorView rendered = _render(widget, constraints, baselineToCalculate: baseline); + return rendered.childBaseline ?? rendered.size.height; + } + + double? measureDistanceToActualBaseline( + Widget widget, { + TextBaseline baseline = TextBaseline.alphabetic, + BoxConstraints constraints = const BoxConstraints(), + }) { + final SurveyorView rendered = _render(widget, constraints, baselineToCalculate: baseline); + return rendered.childBaseline; + } + + SurveyorView _render( + Widget widget, + BoxConstraints constraints, { + TextBaseline? baselineToCalculate, + }) { + bool debugIsPerformingCleanup = false; + final PipelineOwner pipelineOwner = PipelineOwner( + onNeedVisualUpdate: () { + assert(() { + if (!debugIsPerformingCleanup) { + throw FlutterError.fromParts([ + ErrorSummary('Visual update was requested during survey.'), + ErrorDescription('WidgetSurveyor does not support a render object ' + 'calling markNeedsLayout(), markNeedsPaint(), or ' + 'markNeedsSemanticUpdate() while the widget is being surveyed.'), + ]); + } + return true; + }()); + }, + ); + final SurveyorView rootView = pipelineOwner.rootNode = SurveyorView(); + final BuildOwner buildOwner = BuildOwner(focusManager: FocusManager()); + assert(buildOwner.globalKeyCount == 0); + final RenderObjectToWidgetElement element = RenderObjectToWidgetAdapter( + container: rootView, + debugShortDescription: '[root]', + child: widget, + ).attachToRenderTree(buildOwner); + try { + rootView.baselineToCalculate = baselineToCalculate; + rootView.childConstraints = constraints; + rootView.scheduleInitialLayout(); + pipelineOwner.flushLayout(); + assert(rootView.child != null); + return rootView; + } finally { + // Un-mount all child elements to properly clean up. + debugIsPerformingCleanup = true; + try { + element.update(RenderObjectToWidgetAdapter(container: rootView)); + buildOwner.finalizeTree(); + } finally { + debugIsPerformingCleanup = false; + } + assert(buildOwner.globalKeyCount == 1); // RenderObjectToWidgetAdapter uses a global key + } + } +} + +class SurveyorView extends RenderBox with RenderObjectWithChildMixin { + BoxConstraints? childConstraints; + TextBaseline? baselineToCalculate; + double? childBaseline; + + @override + void performLayout() { + assert(child != null); + assert(childConstraints != null); + child!.layout(childConstraints!, parentUsesSize: true); + if (baselineToCalculate != null) { + childBaseline = child!.getDistanceToBaseline(baselineToCalculate!); + } + size = child!.size; + } + + @override + void debugAssertDoesMeetConstraints() => true; +} diff --git a/lib/src/widgets/base_control.dart b/lib/src/widgets/base_control.dart new file mode 100644 index 00000000..b49b7709 --- /dev/null +++ b/lib/src/widgets/base_control.dart @@ -0,0 +1,517 @@ +import 'package:figma_squircle/figma_squircle.dart'; +import 'package:flutter/material.dart'; + +import 'package:moon_design/src/theme/borders.dart'; +import 'package:moon_design/src/theme/colors.dart'; +import 'package:moon_design/src/theme/effects/controls_effects.dart'; +import 'package:moon_design/src/theme/effects/focus_effects.dart'; +import 'package:moon_design/src/theme/effects/hover_effects.dart'; +import 'package:moon_design/src/theme/opacity.dart'; +import 'package:moon_design/src/theme/theme.dart'; +import 'package:moon_design/src/utils/extensions.dart'; +import 'package:moon_design/src/utils/touch_target_padding.dart'; +import 'package:moon_design/src/widgets/effects/focus_effect.dart'; +import 'package:moon_design/src/widgets/effects/pulse_effect.dart'; + +typedef MoonBaseControlBuilder = Widget Function( + BuildContext context, + bool isEnabled, + bool isHovered, + bool isFocused, + bool isPressed, +); + +class MoonBaseControl extends StatefulWidget { + /// The callback that is called when the control is tapped or pressed. + final VoidCallback? onTap; + + /// The callback that is called when the control is long-pressed. + final VoidCallback? onLongPress; + + /// The focus node for this control. + final FocusNode? focusNode; + + /// Whether this control should autofocus when it is first added to the tree. + final bool autofocus; + + /// Whether this control should be focusable. + final bool isFocusable; + + /// Whether this control should ensure that it has a minimal touch target size. + final bool ensureMinimalTouchTargetSize; + + /// Whether this control should show a border. + final bool showBorder; + + /// Whether this control should show a focus effect. + final bool showFocusEffect; + + /// Whether this control should show a pulse effect. + final bool showPulseEffect; + + /// Whether this control should jiggle when the pulse effect is shown. + final bool showPulseEffectJiggle; + + /// Whether this control should show a scale animation. + final bool showScaleAnimation; + + /// Whether the semantic type of this control is button. + final bool semanticTypeIsButton; + + /// The semantic label for this control. + final String? semanticLabel; + + /// The minimum size of the touch target. + final double minTouchTargetSize; + + /// The height of the control. + final double? height; + + /// The border width of the control. + final double? borderWidth; + + /// The opacity of the control when it is disabled. + final double? disabledOpacityValue; + + /// The extent of the focus effect. + final double? focusEffectExtent; + + /// The extent of the pulse effect. + final double? pulseEffectExtent; + + /// The scalar controlling the scaling of the scale effect. + final double? scaleEffectScalar; + + /// The background color of the control. + final Color? backgroundColor; + + /// The border color of the control. + final Color? borderColor; + + /// The text color of the control. + final Color? textColor; + + /// The color of the focus effect. + final Color? focusEffectColor; + + /// The color of the hover effect. + final Color? hoverEffectColor; + + /// The color of the pulse effect. + final Color? pulseEffectColor; + + /// The duration of the focus effect. + final Duration? focusEffectDuration; + + /// The duration of the hover effect. + final Duration? hoverEffectDuration; + + /// The duration of the scale effect. + final Duration? scaleEffectDuration; + + /// The duration of the pulse effect. + final Duration? pulseEffectDuration; + + /// The curve of the focus effect. + final Curve? focusEffectCurve; + + /// The curve of the hover effect. + final Curve? hoverEffectCurve; + + /// The curve of the pulse effect. + final Curve? pulseEffectCurve; + + /// The curve of the scale effect. + final Curve? scaleEffectCurve; + + /// The border radius of the control. + final BorderRadius? borderRadius; + + /// The mouse cursor of the control. + final MouseCursor cursor; + + /// The builder that builds the child of this control. + final MoonBaseControlBuilder builder; + + /// MDS base control widget. + const MoonBaseControl({ + super.key, + this.onTap, + this.onLongPress, + this.focusNode, + this.autofocus = false, + this.isFocusable = true, + this.ensureMinimalTouchTargetSize = false, + this.showBorder = false, + this.showFocusEffect = true, + this.showPulseEffect = false, + this.showPulseEffectJiggle = true, + this.showScaleAnimation = true, + this.semanticTypeIsButton = false, + this.semanticLabel, + this.minTouchTargetSize = 40.0, + this.height, + this.borderWidth, + this.disabledOpacityValue, + this.focusEffectExtent, + this.pulseEffectExtent, + this.scaleEffectScalar, + this.backgroundColor, + this.borderColor, + this.textColor, + this.focusEffectColor, + this.hoverEffectColor, + this.pulseEffectColor, + this.focusEffectDuration, + this.hoverEffectDuration, + this.pulseEffectDuration, + this.scaleEffectDuration, + this.focusEffectCurve, + this.hoverEffectCurve, + this.pulseEffectCurve, + this.scaleEffectCurve, + this.borderRadius = BorderRadius.zero, + this.cursor = MouseCursor.defer, + required this.builder, + }); + + @override + State createState() => _MoonBaseControlState(); +} + +class _MoonBaseControlState extends State { + bool _isFocused = false; + bool _isHovered = false; + bool _isPressed = false; + + FocusNode? _focusNode; + + late Map> _actions; + + bool get _isEnabled => widget.onTap != null || widget.onLongPress != null; + bool get _canAnimateFocus => widget.showFocusEffect && _isEnabled && _isFocused; + bool get _canAnimateHover => _isEnabled && (_isHovered || _isFocused || _isPressed); + bool get _canAnimatePulse => widget.showPulseEffect && _isEnabled; + bool get _canAnimateScale => widget.showScaleAnimation && _isEnabled && _isPressed; + + MouseCursor get _cursor => _isEnabled ? widget.cursor : SystemMouseCursors.forbidden; + FocusNode get _effectiveFocusNode => widget.focusNode ?? (_focusNode ??= FocusNode()); + + void _handleHover(bool hover) { + if (hover != _isHovered && mounted) { + setState(() => _isHovered = hover); + } + } + + void _handleFocus(bool focus) { + if (focus != _isFocused && mounted) { + setState(() => _isFocused = focus); + } + } + + void _handleFocusChange(bool hasFocus) { + setState(() { + _isFocused = hasFocus; + + if (!hasFocus) { + _isPressed = false; + } + }); + } + + Future _handleTap() async { + if (_isEnabled) { + if (mounted) { + setState(() => _isPressed = true); + } + + widget.onTap?.call(); + + if (mounted) { + setState(() => _isPressed = false); + } + } + } + + void _handleTapUp(_) { + if (_isPressed && mounted) { + setState(() => _isPressed = false); + } + } + + void _handleLongPress() { + if (_isEnabled) { + widget.onLongPress?.call(); + } + } + + void _handleLongPressDown(_) { + if (!_isPressed && mounted) { + setState(() => _isPressed = true); + } + } + + void _handleLongPressUp() { + if (_isPressed && mounted) { + setState(() => _isPressed = false); + } + } + + void _handleHorizontalDragStart(DragStartDetails dragStartDetails) => _handleLongPressDown(null); + + void _handleHorizontalDragEnd(DragEndDetails dragEndDetails) => _handleTapUp(null); + + void _handleVerticalDragStart(DragStartDetails dragStartDetails) => _handleLongPressDown(null); + + void _handleVerticalDragEnd(DragEndDetails dragEndDetails) => _handleTapUp(null); + + Color getFocusColor({required bool isDarkMode, required Color focusColor}) { + if (widget.backgroundColor != null) { + return isDarkMode ? widget.backgroundColor!.withOpacity(0.8) : widget.backgroundColor!.withOpacity(0.2); + } else { + return focusColor; + } + } + + Color getTextColor({required bool isDarkMode, required bool isHovered, required bool isFocused}) { + if (widget.textColor != null && (!isHovered && !isFocused)) return widget.textColor!; + if (widget.backgroundColor == null && isDarkMode) return MoonColors.dark.bulma; + if (widget.backgroundColor == null && !isDarkMode) return MoonColors.light.bulma; + + final backgroundLuminance = widget.backgroundColor!.computeLuminance(); + if (backgroundLuminance > 0.5) { + return MoonColors.light.bulma; + } else { + return MoonColors.dark.bulma; + } + } + + @override + void initState() { + super.initState(); + + _actions = >{ActivateIntent: CallbackAction(onInvoke: (_) => _handleTap())}; + + _focusNode = FocusNode(canRequestFocus: _isEnabled); + _effectiveFocusNode.canRequestFocus = _isEnabled; + + if (widget.autofocus) { + _effectiveFocusNode.requestFocus(); + } + } + + @override + void didUpdateWidget(MoonBaseControl oldWidget) { + super.didUpdateWidget(oldWidget); + + if (widget.onTap != oldWidget.onTap || widget.onLongPress != oldWidget.onLongPress) { + if (!_isEnabled) { + _isHovered = _isPressed = false; + } + } + + _effectiveFocusNode.canRequestFocus = _isEnabled; + + if (_isPressed && mounted) { + setState(() => _isPressed = false); + } + } + + @override + void dispose() { + _focusNode!.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final Color effectiveTextColor = + getTextColor(isDarkMode: context.isDarkMode, isHovered: _isHovered, isFocused: _isFocused); + + final double effectiveDisabledOpacityValue = + widget.disabledOpacityValue ?? context.moonOpacity?.disabled ?? MoonOpacity.opacities.disabled; + + // Border props + final BorderRadius effectiveBorderRadius = widget.borderRadius ?? BorderRadius.circular(0); + final Color effectiveBorderColor = widget.borderColor ?? context.moonColors?.trunks ?? MoonColors.light.trunks; + final double effectiveBorderWidth = + widget.borderWidth ?? context.moonBorders?.borderWidth ?? MoonBorders.borders.borderWidth; + + // Focus effect props + final Color effectiveFocusEffectColor = widget.focusEffectColor ?? + context.moonEffects?.controlFocusEffect.effectColor ?? + MoonFocusEffects.lightFocusEffect.effectColor; + + final double effectiveFocusEffectExtent = widget.focusEffectExtent ?? + context.moonEffects?.controlFocusEffect.effectExtent ?? + MoonFocusEffects.darkFocusEffect.effectExtent; + + final Curve effectiveFocusEffectCurve = widget.focusEffectCurve ?? + context.moonEffects?.controlFocusEffect.effectCurve ?? + MoonFocusEffects.lightFocusEffect.effectCurve; + + final Duration effectiveFocusEffectDuration = widget.focusEffectDuration ?? + context.moonEffects?.controlFocusEffect.effectDuration ?? + MoonFocusEffects.lightFocusEffect.effectDuration; + + final Color focusColor = getFocusColor(isDarkMode: context.isDarkMode, focusColor: effectiveFocusEffectColor); + + // Hover effect props + final Color effectiveHoverEffectColor = widget.hoverEffectColor ?? + context.moonEffects?.buttonHoverEffect.primaryHoverColor ?? + MoonHoverEffects.lightButtonHoverEffect.primaryHoverColor; + + final Curve effectiveHoverEffectCurve = widget.hoverEffectCurve ?? + context.moonEffects?.buttonHoverEffect.hoverCurve ?? + MoonHoverEffects.lightButtonHoverEffect.hoverCurve; + + final Duration effectiveHoverEffectDuration = widget.hoverEffectDuration ?? + context.moonEffects?.buttonHoverEffect.hoverDuration ?? + MoonHoverEffects.lightButtonHoverEffect.hoverDuration; + + final Color hoverColor = Color.alphaBlend(effectiveHoverEffectColor, widget.backgroundColor ?? Colors.transparent); + + // Pulse effect props + final Color effectivePulseEffectColor = widget.pulseEffectColor ?? + context.moonEffects?.controlPulseEffect.effectColor ?? + MoonControlsEffects.controlPulseEffect.effectColor!; + + final double effectivePulseEffectExtent = widget.pulseEffectExtent ?? + context.moonEffects?.controlPulseEffect.effectExtent ?? + MoonControlsEffects.controlPulseEffect.effectExtent!; + + final Duration effectivePulseEffectDuration = widget.pulseEffectDuration ?? + context.moonEffects?.controlPulseEffect.effectDuration ?? + MoonControlsEffects.controlPulseEffect.effectDuration; + + final Curve effectivePulseEffectCurve = widget.pulseEffectCurve ?? + context.moonEffects?.controlPulseEffect.effectCurve ?? + MoonControlsEffects.controlPulseEffect.effectCurve; + + // Scale effect props + final double effectiveScaleEffectScalar = widget.scaleEffectScalar ?? + context.moonEffects?.controlScaleEffect.effectScalar ?? + MoonControlsEffects.controlScaleEffect.effectScalar!; + + final Duration effectiveScaleEffectDuration = widget.scaleEffectDuration ?? + context.moonEffects?.controlScaleEffect.effectDuration ?? + MoonControlsEffects.controlScaleEffect.effectDuration; + + final Curve effectiveScaleEffectCurve = widget.scaleEffectCurve ?? + context.moonEffects?.controlScaleEffect.effectCurve ?? + MoonControlsEffects.controlScaleEffect.effectCurve; + + final Widget child = widget.builder( + context, + _isEnabled, + _isHovered, + _isFocused, + _isPressed, + ); + + return RepaintBoundary( + child: MergeSemantics( + child: Semantics( + label: widget.semanticLabel, + button: widget.semanticTypeIsButton, + enabled: _isEnabled, + focusable: _isEnabled, + focused: _isFocused, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: _handleTap, + onLongPress: _handleLongPress, + onLongPressDown: _handleLongPressDown, + onLongPressUp: _handleLongPressUp, + onHorizontalDragStart: _handleHorizontalDragStart, + onHorizontalDragEnd: _handleHorizontalDragEnd, + onVerticalDragStart: _handleVerticalDragStart, + onVerticalDragEnd: _handleVerticalDragEnd, + child: FocusableActionDetector( + enabled: _isEnabled && widget.isFocusable, + focusNode: _effectiveFocusNode, + autofocus: _isEnabled && widget.autofocus, + mouseCursor: _cursor, + onShowHoverHighlight: _handleHover, + onShowFocusHighlight: _handleFocus, + onFocusChange: _handleFocusChange, + actions: _actions, + child: AnimatedDefaultTextStyle( + duration: effectiveHoverEffectDuration, + curve: effectiveHoverEffectCurve, + style: TextStyle(color: effectiveTextColor), + child: TouchTargetPadding( + minSize: widget.ensureMinimalTouchTargetSize + ? Size(widget.minTouchTargetSize, widget.minTouchTargetSize) + : Size.zero, + child: AnimatedScale( + scale: _canAnimateScale ? effectiveScaleEffectScalar : 1, + duration: effectiveScaleEffectDuration, + curve: effectiveScaleEffectCurve, + child: MoonPulseEffect( + show: _canAnimatePulse, + showJiggle: widget.showPulseEffectJiggle, + childBorderRadius: widget.borderRadius, + effectColor: effectivePulseEffectColor, + effectExtent: effectivePulseEffectExtent, + effectCurve: effectivePulseEffectCurve, + effectDuration: effectivePulseEffectDuration, + child: AnimatedOpacity( + opacity: _isEnabled ? 1 : effectiveDisabledOpacityValue, + duration: const Duration(milliseconds: 150), + curve: Curves.easeInOut, + child: AnimatedContainer( + height: widget.height, + duration: effectiveHoverEffectDuration, + curve: effectiveHoverEffectCurve, + decoration: ShapeDecoration( + color: _canAnimateHover ? hoverColor : widget.backgroundColor, + shape: SmoothRectangleBorder( + side: BorderSide( + color: effectiveBorderColor, + width: widget.showBorder ? effectiveBorderWidth : 0, + style: widget.showBorder ? BorderStyle.solid : BorderStyle.none, + ), + 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: MoonFocusEffect( + show: _canAnimateFocus, + effectColor: focusColor, + effectExtent: effectiveFocusEffectExtent, + effectCurve: effectiveFocusEffectCurve, + effectDuration: effectiveFocusEffectDuration, + childBorderRadius: widget.borderRadius, + child: child, + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/src/widgets/buttons/button.dart b/lib/src/widgets/buttons/button.dart new file mode 100644 index 00000000..8a0fc453 --- /dev/null +++ b/lib/src/widgets/buttons/button.dart @@ -0,0 +1,324 @@ +import 'package:flutter/material.dart'; + +import 'package:moon_design/src/theme/button_sizes.dart'; +import 'package:moon_design/src/theme/theme.dart'; +import 'package:moon_design/src/widgets/base_control.dart'; + +enum ButtonSize { + xs, + sm, + md, + lg, + xl, +} + +class MoonButton extends StatelessWidget { + /// The callback that is called when the control is tapped or pressed. + final VoidCallback? onTap; + + /// The callback that is called when the control is long-pressed. + final VoidCallback? onLongPress; + + /// The size of the button. + final ButtonSize? buttonSize; + + /// The focus node for the button. + final FocusNode? focusNode; + + /// The semantic label for the button. + final String? semanticLabel; + + /// The width of the button. + final double? width; + + /// The height of the button. + final double? height; + + /// The opacity value of the button when it is disabled. + final double? disabledOpacityValue; + + /// The border width of the button. + final double? borderWidth; + + /// The gap between the icon and the label. + final double? gap; + + /// The extent of the focus effect. + final double? focusEffectExtent; + + /// The extent of the pulse effect. + final double? pulseEffectExtent; + + /// The scalar controlling the scaling of the scale effect. + final double? scaleEffectScalar; + + /// The minimum size of the touch target. + final double minTouchTargetSize; + + /// Whether the button should automatically be focused when it is mounted. + final bool autofocus; + + /// Whether the button should be focusable. + final bool isFocusable; + + /// Whether the button should be full width. + final bool isFullWidth; + + /// Whether this button should ensure that it has a minimal touch target size. + final bool ensureMinimalTouchTargetSize; + + /// Whether the button should show a border. + final bool showBorder; + + /// Whether the button should show a focus effect. + final bool showFocusEffect; + + /// Whether the button should show a pulse effect. + final bool showPulseEffect; + + /// Whether the button should jiggle when the pulse effect is shown. + final bool showPulseEffectJiggle; + + /// Whether the button should show a scale animation. + final bool showScaleAnimation; + + /// The background color of the button. + final Color? backgroundColor; + + /// The border color of the button. + final Color? borderColor; + + /// The text color of the button. + final Color? textColor; + + /// The color of the focus effect. + final Color? focusEffectColor; + + /// The color of the hover effect. + final Color? hoverEffectColor; + + /// The color of the pulse effect. + final Color? pulseEffectColor; + + /// The duration of the focus effect. + final Duration? focusEffectDuration; + + /// The duration of the hover effect. + final Duration? hoverEffectDuration; + + /// The duration of the scale effect. + final Duration? scaleEffectDuration; + + /// The duration of the pulse effect. + final Duration? pulseEffectDuration; + + /// The curve of the focus effect. + final Curve? focusEffectCurve; + + /// The curve of the hover effect. + final Curve? hoverEffectCurve; + + /// The curve of the scale effect. + final Curve? scaleEffectCurve; + + /// The curve of the pulse effect. + final Curve? pulseEffectCurve; + + /// The padding of the button. + final EdgeInsets? padding; + + /// The border radius of the button. + final BorderRadius? borderRadius; + + /// The widget in the left icon slot of the button. + final Widget? leftIcon; + + /// The widget in the label slot of the button. + final Widget? label; + + /// The widget in the right icon slot of the button. + final Widget? rightIcon; + + /// MDS base button. + /// + /// See also: + /// + /// * [MoonPrimaryButton], MDS primary button. + /// * [MoonSecondaryButton], MDS secondary button. + /// * [MoonTertiaryButton], MDS tertiary button. + /// * [MoonGhostButton], MDS ghost button. + const MoonButton({ + super.key, + this.onTap, + this.onLongPress, + this.buttonSize, + this.focusNode, + this.semanticLabel, + this.width, + this.height, + this.disabledOpacityValue, + this.borderWidth, + this.gap, + this.focusEffectExtent, + this.pulseEffectExtent, + this.scaleEffectScalar, + this.minTouchTargetSize = 40, + this.autofocus = false, + this.isFocusable = true, + this.isFullWidth = false, + this.ensureMinimalTouchTargetSize = false, + this.showBorder = false, + this.showFocusEffect = true, + this.showPulseEffect = false, + this.showPulseEffectJiggle = true, + this.showScaleAnimation = true, + this.backgroundColor, + this.borderColor, + this.textColor, + this.focusEffectColor, + this.hoverEffectColor, + this.pulseEffectColor, + this.focusEffectDuration, + this.hoverEffectDuration, + this.scaleEffectDuration, + this.pulseEffectDuration, + this.focusEffectCurve, + this.hoverEffectCurve, + this.scaleEffectCurve, + this.pulseEffectCurve, + this.padding, + this.borderRadius, + this.label, + this.leftIcon, + this.rightIcon, + }); + + MoonButtonSizes getButtonSize(BuildContext context, ButtonSize? buttonSize) { + switch (buttonSize) { + case ButtonSize.xs: + return context.moonTheme?.buttonTheme.xs ?? MoonButtonSizes.xs; + case ButtonSize.sm: + return context.moonTheme?.buttonTheme.sm ?? MoonButtonSizes.sm; + case ButtonSize.md: + return context.moonTheme?.buttonTheme.md ?? MoonButtonSizes.md; + case ButtonSize.lg: + return context.moonTheme?.buttonTheme.lg ?? MoonButtonSizes.lg; + case ButtonSize.xl: + return context.moonTheme?.buttonTheme.xl ?? MoonButtonSizes.xl; + default: + return context.moonTheme?.buttonTheme.md ?? MoonButtonSizes.xs; + } + } + + @override + Widget build(BuildContext context) { + final MoonButtonSizes effectiveButtonSize = getButtonSize(context, buttonSize); + final BorderRadius effectiveBorderRadius = borderRadius ?? effectiveButtonSize.borderRadius; + + final double effectiveGap = gap ?? effectiveButtonSize.gap; + final double effectiveHeight = height ?? effectiveButtonSize.height; + final EdgeInsets effectivePadding = padding ?? effectiveButtonSize.padding; + + final EdgeInsetsDirectional correctedPadding = EdgeInsetsDirectional.fromSTEB( + leftIcon == null && label != null ? effectivePadding.left : 0, + effectivePadding.top, + rightIcon == null && label != null ? effectivePadding.right : 0, + effectivePadding.bottom, + ); + + return MoonBaseControl( + onTap: onTap, + onLongPress: onLongPress, + semanticLabel: semanticLabel, + semanticTypeIsButton: true, + borderRadius: effectiveBorderRadius, + disabledOpacityValue: disabledOpacityValue, + minTouchTargetSize: minTouchTargetSize, + ensureMinimalTouchTargetSize: ensureMinimalTouchTargetSize, + focusNode: focusNode, + autofocus: autofocus, + isFocusable: isFocusable, + showBorder: showBorder, + showFocusEffect: showFocusEffect, + backgroundColor: backgroundColor, + borderColor: borderColor, + height: effectiveHeight, + borderWidth: borderWidth, + textColor: textColor, + focusEffectColor: focusEffectColor, + focusEffectExtent: focusEffectExtent, + focusEffectDuration: focusEffectDuration, + focusEffectCurve: focusEffectCurve, + hoverEffectColor: hoverEffectColor, + hoverEffectDuration: hoverEffectDuration, + hoverEffectCurve: hoverEffectCurve, + showScaleAnimation: showScaleAnimation, + scaleEffectScalar: scaleEffectScalar, + scaleEffectDuration: scaleEffectDuration, + scaleEffectCurve: scaleEffectCurve, + showPulseEffect: showPulseEffect, + showPulseEffectJiggle: showPulseEffectJiggle, + pulseEffectColor: pulseEffectColor, + pulseEffectExtent: pulseEffectExtent, + pulseEffectDuration: pulseEffectDuration, + pulseEffectCurve: pulseEffectCurve, + builder: (context, isEnabled, isHovered, isFocused, isPressed) { + return AnimatedContainer( + duration: const Duration(milliseconds: 150), + curve: Curves.easeInOut, + width: width, + height: effectiveHeight, + padding: correctedPadding, + constraints: BoxConstraints(minWidth: effectiveHeight), + child: DefaultTextStyle.merge( + style: TextStyle(fontSize: effectiveButtonSize.textStyle.fontSize), + child: isFullWidth + ? Stack( + fit: StackFit.expand, + alignment: Alignment.center, + children: [ + if (leftIcon != null) + Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: EdgeInsets.symmetric(horizontal: effectiveGap), + child: leftIcon, + ), + ), + if (label != null) + Align( + child: label, + ), + if (rightIcon != null) + Align( + alignment: Alignment.centerRight, + child: Padding( + padding: EdgeInsets.symmetric(horizontal: effectiveGap), + child: rightIcon, + ), + ), + ], + ) + : 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/buttons/ghost_button.dart b/lib/src/widgets/buttons/ghost_button.dart new file mode 100644 index 00000000..fdb22cb6 --- /dev/null +++ b/lib/src/widgets/buttons/ghost_button.dart @@ -0,0 +1,113 @@ +import 'package:flutter/material.dart'; + +import 'package:moon_design/src/theme/colors.dart'; +import 'package:moon_design/src/theme/theme.dart'; +import 'package:moon_design/src/utils/extensions.dart'; +import 'package:moon_design/src/widgets/buttons/button.dart'; + +class MoonGhostButton extends StatelessWidget { + /// The callback that is called when the button is tapped or pressed. + final VoidCallback? onTap; + + /// The callback that is called when the button is long-pressed. + final VoidCallback? onLongPress; + + /// The size of the button. + final ButtonSize? buttonSize; + + /// The focus node for the button. + final FocusNode? focusNode; + + /// The semantic label for the button. + final String? semanticLabel; + + /// The width of the button. + final double? width; + + /// The height of the button. + final double? height; + + /// The minimum size of the touch target. + final double minTouchTargetSize; + + /// 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. + final bool autofocus; + + /// Whether this button should be focusable. + final bool isFocusable; + + /// Whether this button should be full width. + final bool isFullWidth; + + /// Whether this button should show a pulse effect. + final bool showPulseEffect; + + /// The widget in the left icon slot of the button. + final Widget? leftIcon; + + /// The widget in the label slot of the button. + final Widget? label; + + /// The widget in the right icon slot of the button. + final Widget? rightIcon; + + /// MDS ghost button variant. + /// + /// See also: + /// + /// * [MoonPrimaryButton], MDS primary button. + /// * [MoonTertiaryButton], MDS tertiary button. + /// * [MoonTertiaryButton], MDS tertiary button. + const MoonGhostButton({ + super.key, + this.onTap, + this.onLongPress, + this.buttonSize, + this.focusNode, + this.semanticLabel, + this.width, + this.height, + this.minTouchTargetSize = 40, + this.ensureMinimalTouchTargetSize = false, + this.autofocus = false, + this.isFocusable = true, + this.isFullWidth = false, + this.showPulseEffect = false, + this.label, + this.leftIcon, + this.rightIcon, + }); + + @override + Widget build(BuildContext context) { + final effectiveTextColor = context.moonColors?.trunks ?? MoonColors.light.trunks; + final effectiveHoverColor = context.moonColors?.jiren ?? MoonColors.light.jiren; + final effectiveFocusColor = context.moonColors?.piccolo.withOpacity(context.isDarkMode ? 0.8 : 0.2) ?? + MoonColors.light.piccolo.withOpacity(context.isDarkMode ? 0.8 : 0.2); + + return MoonButton( + onTap: onTap, + onLongPress: onLongPress, + buttonSize: buttonSize, + focusNode: focusNode, + semanticLabel: semanticLabel, + width: width, + height: height, + minTouchTargetSize: minTouchTargetSize, + ensureMinimalTouchTargetSize: ensureMinimalTouchTargetSize, + autofocus: autofocus, + isFocusable: isFocusable, + isFullWidth: isFullWidth, + textColor: effectiveTextColor, + showPulseEffect: showPulseEffect, + hoverEffectColor: effectiveHoverColor, + focusEffectColor: effectiveFocusColor, + label: label, + leftIcon: leftIcon, + rightIcon: rightIcon, + ); + } +} diff --git a/lib/src/widgets/buttons/primary_button.dart b/lib/src/widgets/buttons/primary_button.dart new file mode 100644 index 00000000..efd07faa --- /dev/null +++ b/lib/src/widgets/buttons/primary_button.dart @@ -0,0 +1,107 @@ +import 'package:flutter/material.dart'; + +import 'package:moon_design/src/theme/colors.dart'; +import 'package:moon_design/src/theme/theme.dart'; +import 'package:moon_design/src/widgets/buttons/button.dart'; + +/// MDS primary button variant. +/// +/// See also: +/// +/// * [MoonSecondaryButton], MDS secondary button. +/// * [MoonTertiaryButton], MDS tertiary button. +/// * [MoonGhostButton], MDS ghost button. +class MoonPrimaryButton extends StatelessWidget { + /// The callback that is called when the button is tapped or pressed. + final VoidCallback? onTap; + + /// The callback that is called when the button is long-pressed. + final VoidCallback? onLongPress; + + /// The size of the button. + final ButtonSize? buttonSize; + + /// The focus node for the button. + final FocusNode? focusNode; + + /// The semantic label for the button. + final String? semanticLabel; + + /// The width of the button. + final double? width; + + /// The height of the button. + final double? height; + + /// The minimum size of the touch target. + final double minTouchTargetSize; + + /// 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. + final bool autofocus; + + /// Whether this button should be focusable. + final bool isFocusable; + + /// Whether this button should be full width. + final bool isFullWidth; + + /// Whether this button should show a pulse effect. + final bool showPulseEffect; + + /// The widget in the left icon slot of the button. + final Widget? leftIcon; + + /// The widget in the label slot of the button. + final Widget? label; + + /// The widget in the right icon slot of the button. + final Widget? rightIcon; + + const MoonPrimaryButton({ + super.key, + this.onTap, + this.onLongPress, + this.buttonSize, + this.focusNode, + this.semanticLabel, + this.width, + this.height, + this.minTouchTargetSize = 40, + this.ensureMinimalTouchTargetSize = false, + this.autofocus = false, + this.isFocusable = true, + this.isFullWidth = false, + this.showPulseEffect = false, + this.label, + this.leftIcon, + this.rightIcon, + }); + + @override + Widget build(BuildContext context) { + final effectiveBackgroundColor = context.moonColors?.piccolo ?? MoonColors.light.piccolo; + + return MoonButton( + onTap: onTap, + onLongPress: onLongPress, + buttonSize: buttonSize, + backgroundColor: effectiveBackgroundColor, + focusNode: focusNode, + semanticLabel: semanticLabel, + width: width, + height: height, + minTouchTargetSize: minTouchTargetSize, + ensureMinimalTouchTargetSize: ensureMinimalTouchTargetSize, + autofocus: autofocus, + isFocusable: isFocusable, + isFullWidth: isFullWidth, + showPulseEffect: showPulseEffect, + label: label, + leftIcon: leftIcon, + rightIcon: rightIcon, + ); + } +} diff --git a/lib/src/widgets/buttons/secondary_button.dart b/lib/src/widgets/buttons/secondary_button.dart new file mode 100644 index 00000000..7e5585d9 --- /dev/null +++ b/lib/src/widgets/buttons/secondary_button.dart @@ -0,0 +1,103 @@ +import 'package:flutter/material.dart'; + +import 'package:moon_design/src/widgets/buttons/button.dart'; + +class MoonSecondaryButton extends StatelessWidget { + /// The callback that is called when the button is tapped or pressed. + final VoidCallback? onTap; + + /// The callback that is called when the button is long-pressed. + final VoidCallback? onLongPress; + + /// The size of the button. + final ButtonSize? buttonSize; + + /// The focus node for the button. + final FocusNode? focusNode; + + /// The semantic label for the button. + final String? semanticLabel; + + /// The width of the button. + final double? width; + + /// The height of the button. + final double? height; + + /// The minimum size of the touch target. + final double minTouchTargetSize; + + /// 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. + final bool autofocus; + + /// Whether this button should be focusable. + final bool isFocusable; + + /// Whether this button should be full width. + final bool isFullWidth; + + /// Whether this button should show a pulse effect. + final bool showPulseEffect; + + /// The widget in the left icon slot of the button. + final Widget? leftIcon; + + /// The widget in the label slot of the button. + final Widget? label; + + /// The widget in the right icon slot of the button. + final Widget? rightIcon; + + /// MDS secondary button variant. + /// + /// See also: + /// + /// * [MoonPrimaryButton], MDS primary button. + /// * [MoonTertiaryButton], MDS tertiary button. + /// * [MoonGhostButton], MDS ghost button. + const MoonSecondaryButton({ + super.key, + this.onTap, + this.onLongPress, + this.buttonSize, + this.focusNode, + this.semanticLabel, + this.width, + this.height, + this.minTouchTargetSize = 40, + this.ensureMinimalTouchTargetSize = false, + this.autofocus = false, + this.isFocusable = true, + this.isFullWidth = false, + this.showPulseEffect = false, + this.label, + this.leftIcon, + this.rightIcon, + }); + + @override + Widget build(BuildContext context) { + return MoonButton( + onTap: onTap, + onLongPress: onLongPress, + buttonSize: buttonSize, + focusNode: focusNode, + semanticLabel: semanticLabel, + width: width, + height: height, + minTouchTargetSize: minTouchTargetSize, + ensureMinimalTouchTargetSize: ensureMinimalTouchTargetSize, + autofocus: autofocus, + isFocusable: isFocusable, + isFullWidth: isFullWidth, + showPulseEffect: showPulseEffect, + showBorder: true, + label: label, + leftIcon: leftIcon, + rightIcon: rightIcon, + ); + } +} diff --git a/lib/src/widgets/buttons/tertiary_button.dart b/lib/src/widgets/buttons/tertiary_button.dart new file mode 100644 index 00000000..4237a2be --- /dev/null +++ b/lib/src/widgets/buttons/tertiary_button.dart @@ -0,0 +1,109 @@ +import 'package:flutter/material.dart'; + +import 'package:moon_design/src/theme/colors.dart'; +import 'package:moon_design/src/theme/theme.dart'; +import 'package:moon_design/src/widgets/buttons/button.dart'; + +class MoonTertiaryButton extends StatelessWidget { + /// The callback that is called when the button is tapped or pressed. + final VoidCallback? onTap; + + /// The callback that is called when the button is long-pressed. + final VoidCallback? onLongPress; + + /// The size of the button. + final ButtonSize? buttonSize; + + /// The focus node for the button. + final FocusNode? focusNode; + + /// The semantic label for the button. + final String? semanticLabel; + + /// The width of the button. + final double? width; + + /// The height of the button. + final double? height; + + /// The minimum size of the touch target. + final double minTouchTargetSize; + + /// 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. + final bool autofocus; + + /// Whether this button should be focusable. + final bool isFocusable; + + /// Whether this button should be full width. + final bool isFullWidth; + + /// Whether this button should show a pulse effect. + final bool showPulseEffect; + + /// The widget in the left icon slot of the button. + final Widget? leftIcon; + + /// The widget in the label slot of the button. + final Widget? label; + + /// The widget in the right icon slot of the button. + final Widget? rightIcon; + + /// MDS tertiary button variant. + /// + /// See also: + /// + /// * [MoonPrimaryButton], MDS primary button. + /// * [MoonSecondaryButton], MDS secondary button. + /// * [MoonGhostButton], MDS ghost button. + const MoonTertiaryButton({ + super.key, + this.onTap, + this.onLongPress, + this.buttonSize, + this.focusNode, + this.semanticLabel, + this.width, + this.height, + this.minTouchTargetSize = 40, + this.ensureMinimalTouchTargetSize = false, + this.autofocus = false, + this.isFocusable = true, + this.isFullWidth = false, + this.showPulseEffect = false, + this.label, + this.leftIcon, + this.rightIcon, + }); + + @override + Widget build(BuildContext context) { + final effectiveBackgroundColor = context.moonColors?.hit ?? MoonColors.light.hit; + final effectiveBorderColor = context.moonColors?.hit ?? MoonColors.light.hit; + + return MoonButton( + onTap: onTap, + onLongPress: onLongPress, + buttonSize: buttonSize, + backgroundColor: effectiveBackgroundColor, + borderColor: effectiveBorderColor, + focusNode: focusNode, + semanticLabel: semanticLabel, + width: width, + height: height, + minTouchTargetSize: minTouchTargetSize, + ensureMinimalTouchTargetSize: ensureMinimalTouchTargetSize, + autofocus: autofocus, + isFocusable: isFocusable, + isFullWidth: isFullWidth, + showPulseEffect: showPulseEffect, + label: label, + leftIcon: leftIcon, + rightIcon: rightIcon, + ); + } +} diff --git a/lib/src/widgets/effects/focus_effect.dart b/lib/src/widgets/effects/focus_effect.dart new file mode 100644 index 00000000..b290785c --- /dev/null +++ b/lib/src/widgets/effects/focus_effect.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; + +import 'package:moon_design/src/utils/max_border_radius.dart'; +import 'package:moon_design/src/widgets/effects/painters/focus_effect_painter.dart'; + +class MoonFocusEffect extends StatefulWidget { + final bool show; + final double effectExtent; + final Color effectColor; + final Curve effectCurve; + final Duration effectDuration; + final BorderRadius? childBorderRadius; + final Widget child; + + const MoonFocusEffect({ + super.key, + required this.show, + required this.effectExtent, + required this.effectColor, + required this.effectCurve, + required this.effectDuration, + this.childBorderRadius, + required this.child, + }); + + @override + State createState() => _MoonFocusEffectState(); +} + +class _MoonFocusEffectState extends State with SingleTickerProviderStateMixin { + late AnimationController _animationController; + late CurvedAnimation _focusAnimation; + + @override + void initState() { + super.initState(); + + if (mounted) { + _animationController = AnimationController( + vsync: this, + duration: widget.effectDuration, + debugLabel: "MoonFocusEffect animation controller", + ); + + _focusAnimation = CurvedAnimation( + parent: _animationController, + curve: widget.effectCurve, + ); + } + } + + @override + void didUpdateWidget(MoonFocusEffect oldWidget) { + super.didUpdateWidget(oldWidget); + + if (mounted) { + if (widget.show) { + _animationController.forward(); + } else { + _animationController.reverse(); + } + } + } + + @override + void dispose() { + _animationController.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final double focusEffectBorderRadius = maxBorderRadius(widget.childBorderRadius); + + return RepaintBoundary( + child: AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + return CustomPaint( + willChange: true, + painter: FocusEffectPainter( + color: widget.effectColor, + effectExtent: widget.effectExtent, + borderRadiusValue: focusEffectBorderRadius, + animation: _focusAnimation, + ), + child: child, + ); + }, + child: widget.child, + ), + ); + } +} diff --git a/lib/src/widgets/effects/painters/focus_effect_painter.dart b/lib/src/widgets/effects/painters/focus_effect_painter.dart new file mode 100644 index 00000000..b42f96d9 --- /dev/null +++ b/lib/src/widgets/effects/painters/focus_effect_painter.dart @@ -0,0 +1,54 @@ +import 'package:figma_squircle/figma_squircle.dart'; +import 'package:flutter/material.dart'; + +class FocusEffectPainter extends CustomPainter { + final Color color; + final Animation animation; + final double effectExtent; + final double borderRadiusValue; + + FocusEffectPainter({ + required this.color, + required this.animation, + required this.effectExtent, + required this.borderRadiusValue, + }) : super(repaint: animation); + + @override + void paint(Canvas canvas, Size size) { + if (!animation.isDismissed) { + final Rect rect = Rect.fromLTRB(0.0, 0.0, size.width, size.height); + final Color transformedColor = Color.lerp(null, color, animation.value)!; + final double newWidth = rect.width + effectExtent; + final double newHeight = rect.height + effectExtent; + final double widthIncrease = newWidth / rect.width; + final double heightIncrease = newHeight / rect.height; + final double widthOffset = (widthIncrease - 1) / 2; + final double heightOffset = (heightIncrease - 1) / 2; + final double endBorderRadius = borderRadiusValue > 0 ? borderRadiusValue + (effectExtent / 2) : 0; + + final Paint paint = Paint() + ..color = transformedColor + ..style = PaintingStyle.stroke + ..strokeWidth = effectExtent + 1; // +1 for squircle hairline border correction + + canvas.drawRRect( + RRect.fromRectAndRadius( + Rect.fromLTWH( + -rect.width * widthOffset, + -rect.height * heightOffset, + rect.width * widthIncrease, + rect.height * heightIncrease, + ), + SmoothRadius(cornerRadius: endBorderRadius, cornerSmoothing: 1), + ), + paint, + ); + } + } + + @override + bool shouldRepaint(FocusEffectPainter oldDelegate) { + return animation != oldDelegate.animation || color != oldDelegate.color; + } +} diff --git a/lib/src/widgets/effects/painters/pulse_effect_painter.dart b/lib/src/widgets/effects/painters/pulse_effect_painter.dart new file mode 100644 index 00000000..fb78a164 --- /dev/null +++ b/lib/src/widgets/effects/painters/pulse_effect_painter.dart @@ -0,0 +1,71 @@ +import 'dart:ui'; + +import 'package:figma_squircle/figma_squircle.dart'; +import 'package:flutter/animation.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/rendering.dart'; + +class PulseEffectPainter extends CustomPainter { + static const double _animationRangeStartValue = 0.286; + final Color color; + final Animation animation; + final double effectExtent; + final double borderRadiusValue; + + PulseEffectPainter({ + required this.color, + required this.animation, + required this.effectExtent, + required this.borderRadiusValue, + }) : super(repaint: animation); + + double animationRange({ + required double begin, + required double end, + required double animationValue, + }) { + return clampDouble((animationValue - begin) / (end - begin), 0.0, 1.0); + } + + @override + void paint(Canvas canvas, Size size) { + if (!animation.isDismissed) { + final double rangeValue = + animationRange(begin: _animationRangeStartValue, end: 1.0, animationValue: animation.value); + final Rect rect = Rect.fromLTRB(0.0, 0.0, size.width, size.height); + final double opacity = (rangeValue == 0.0 ? 0.0 : 1.0 - rangeValue).clamp(0.0, 1.0); + final Color transformedColor = color.withOpacity(opacity); + final double newWidth = rect.width + rangeValue * effectExtent; + final double newHeight = rect.height + rangeValue * effectExtent; + final double widthIncrease = newWidth / rect.width; + final double heightIncrease = newHeight / rect.height; + final double widthOffset = (widthIncrease - 1) / 2; + final double heightOffset = (heightIncrease - 1) / 2; + final double endBorderRadius = borderRadiusValue > 0 ? borderRadiusValue + (effectExtent / 2) : 0; + final double borderValueLerp = lerpDouble(borderRadiusValue, endBorderRadius, rangeValue)!; + + final Paint paint = Paint() + ..color = transformedColor + ..style = PaintingStyle.stroke + ..strokeWidth = rangeValue * effectExtent + 1; // +1 for squircle hairline border correction + + canvas.drawRRect( + RRect.fromRectAndRadius( + Rect.fromLTWH( + -rect.width * widthOffset, + -rect.height * heightOffset, + rect.width * widthIncrease, + rect.height * heightIncrease, + ), + SmoothRadius(cornerRadius: borderValueLerp, cornerSmoothing: 1), + ), + paint, + ); + } + } + + @override + bool shouldRepaint(PulseEffectPainter oldDelegate) { + return false; + } +} diff --git a/lib/src/widgets/effects/pulse_effect.dart b/lib/src/widgets/effects/pulse_effect.dart new file mode 100644 index 00000000..f58962a1 --- /dev/null +++ b/lib/src/widgets/effects/pulse_effect.dart @@ -0,0 +1,134 @@ +import 'package:flutter/material.dart'; + +import 'package:moon_design/src/utils/max_border_radius.dart'; +import 'package:moon_design/src/widgets/effects/painters/pulse_effect_painter.dart'; + +class MoonPulseEffect extends StatefulWidget { + final bool show; + final bool showJiggle; + final double effectExtent; + final Color effectColor; + final Curve effectCurve; + final Duration effectDuration; + final BorderRadius? childBorderRadius; + final Widget child; + + const MoonPulseEffect({ + super.key, + required this.show, + required this.showJiggle, + required this.effectExtent, + required this.effectColor, + required this.effectCurve, + required this.effectDuration, + this.childBorderRadius, + required this.child, + }); + + @override + State createState() => _MoonPulseEffectState(); +} + +class _MoonPulseEffectState extends State with SingleTickerProviderStateMixin { + static const double _jiggleTimePercentage = 28.6; + static const double _jiggleRestTimePercentage = 100 - _jiggleTimePercentage * 2; + + late AnimationController _animationController; + late CurvedAnimation _pulseAnimation; + late Animation _jiggleAnimation; + + @override + void initState() { + super.initState(); + + if (mounted) { + _animationController = AnimationController( + animationBehavior: AnimationBehavior.preserve, + vsync: this, + duration: widget.effectDuration, + debugLabel: "MoonPulseEffect animation controller", + ); + + _pulseAnimation = CurvedAnimation( + parent: _animationController, + curve: widget.effectCurve, + ); + + _jiggleAnimation = TweenSequence( + [ + TweenSequenceItem( + tween: Tween(begin: 0.0, end: -1.0).chain(CurveTween(curve: widget.effectCurve)), + weight: _jiggleRestTimePercentage / 2, + ), + TweenSequenceItem( + tween: Tween(begin: -1.0, end: 0.0).chain(CurveTween(curve: widget.effectCurve)), + weight: _jiggleRestTimePercentage / 2, + ), + TweenSequenceItem( + tween: ConstantTween(0.0), + weight: _jiggleRestTimePercentage, + ), + TweenSequenceItem( + tween: Tween(begin: 0.0, end: -1.0).chain(CurveTween(curve: widget.effectCurve)), + weight: _jiggleRestTimePercentage / 2, + ), + TweenSequenceItem( + tween: Tween(begin: -1.0, end: 0.0).chain(CurveTween(curve: widget.effectCurve)), + weight: _jiggleRestTimePercentage / 2, + ), + ], + ).animate(_animationController); + } + } + + @override + void didUpdateWidget(covariant MoonPulseEffect oldWidget) { + super.didUpdateWidget(oldWidget); + + if (widget.show != oldWidget.show) { + if (widget.show) { + _animationController.repeat(); + } else { + _animationController.forward().then((_) => _animationController.reset()); + } + } + } + + @override + void dispose() { + _animationController.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final double pulseEffectEndBorderRadius = maxBorderRadius(widget.childBorderRadius); + + // TODO: Review at a later date (when Impeller is stable?) if CurvedAnimation with Interval can be used. Currently + //interval has a bug where the curve parameters curve.transform(t) internal method causes uneven buggy animation. + + return RepaintBoundary( + child: AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + return Transform.translate( + offset: Offset(widget.showJiggle ? _jiggleAnimation.value : 0.0, 0.0), + child: CustomPaint( + isComplex: true, + willChange: true, + painter: PulseEffectPainter( + color: widget.effectColor, + effectExtent: widget.effectExtent, + borderRadiusValue: pulseEffectEndBorderRadius, + animation: _pulseAnimation, + ), + child: child, + ), + ); + }, + child: widget.child, + ), + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 9b20bdfd..1d037456 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -8,6 +8,7 @@ environment: flutter: ">=1.17.0" dependencies: + figma_squircle: ^0.5.3 flutter: sdk: flutter