diff --git a/example/lib/src/storybook/stories/segmented_control.dart b/example/lib/src/storybook/stories/segmented_control.dart index 33b6ebf0..b9002d5f 100644 --- a/example/lib/src/storybook/stories/segmented_control.dart +++ b/example/lib/src/storybook/stories/segmented_control.dart @@ -85,12 +85,7 @@ class SegmentedControlStory extends Story { description: "Gap between MoonSegmentedControl segments.", enabled: false, initial: 4, - max: 16, - ); - - final isExpandedKnob = context.knobs.boolean( - label: "isExpanded", - description: "Whether MoonSegmentControl is horizontally expanded.", + max: 12, ); final showLeadingKnob = context.knobs.boolean( @@ -109,6 +104,16 @@ class SegmentedControlStory extends Story { description: "Show widget in MoonSegmentedControl trailing slot.", ); + final isExpandedKnob = context.knobs.boolean( + label: "isExpanded", + description: "Expand MoonSegmentControl horizontally.", + ); + + final isDisabledKnob = context.knobs.boolean( + label: "isDisabled", + description: "Disable MoonSegmentedControl.", + ); + final segmentStyle = SegmentStyle( textColor: textColor, selectedTextColor: selectedTextColor, @@ -128,6 +133,7 @@ class SegmentedControlStory extends Story { Column( children: [ MoonSegmentedControl( + isDisabled: isDisabledKnob, isExpanded: isExpandedKnob, gap: gapKnob?.toDouble(), segmentedControlSize: segmentedControlSizesKnob, @@ -159,6 +165,7 @@ class SegmentedControlStory extends Story { const TextDivider(text: "Icon MoonSegmentedControl"), const SizedBox(height: 32), MoonSegmentedControl( + isDisabled: isDisabledKnob, isExpanded: isExpandedKnob, gap: gapKnob?.toDouble(), segmentedControlSize: segmentedControlSizesKnob, diff --git a/example/lib/src/storybook/stories/tab_bar.dart b/example/lib/src/storybook/stories/tab_bar.dart new file mode 100644 index 00000000..de6e4581 --- /dev/null +++ b/example/lib/src/storybook/stories/tab_bar.dart @@ -0,0 +1,225 @@ +import 'package:example/src/storybook/common/color_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 TabBarStory extends Story { + TabBarStory() + : super( + name: "TabBar", + builder: (context) { + final tabsSizesKnob = context.knobs.nullable.options( + label: "tabBarSize", + description: "Size variants for MoonTabBar.", + enabled: false, + initial: MoonTabBarSize.md, + options: const [ + Option(label: "sm", value: MoonTabBarSize.sm), + Option(label: "md", value: MoonTabBarSize.md), + ], + ); + + final textColorsKnob = context.knobs.nullable.options( + label: "textColor", + description: "MoonColors variants for default text.", + enabled: false, + initial: 0, + // piccolo + options: colorOptions, + ); + + final textColor = colorTable(context)[textColorsKnob ?? 40]; + + final selectedTextColorsKnob = context.knobs.nullable.options( + label: "selectedTextColor", + description: "MoonColors variants for selected tab text.", + enabled: false, + initial: 0, + // piccolo + options: colorOptions, + ); + + final selectedTextColor = colorTable(context)[selectedTextColorsKnob ?? 40]; + + final indicatorColorsKnob = context.knobs.nullable.options( + label: "indicatorColor", + description: "MoonColors variants for default MoonTabBar indicator.", + enabled: false, + initial: 0, + // piccolo + options: colorOptions, + ); + + final indicatorColor = colorTable(context)[indicatorColorsKnob ?? 40]; + + final selectedTabColorsKnob = context.knobs.nullable.options( + label: "selectedTabColor", + description: "MoonColors variants for pill MoonTabBar selected tab.", + enabled: false, + initial: 0, + // piccolo + options: colorOptions, + ); + + final selectedTabColor = colorTable(context)[selectedTabColorsKnob ?? 40]; + + final borderRadiusKnob = context.knobs.nullable.sliderInt( + label: "borderRadius", + description: "Border radius for pill MoonTabBar.", + enabled: false, + initial: 8, + max: 32, + ); + + final indicatorHeightKnob = context.knobs.nullable.sliderInt( + label: "indicatorHeight", + description: "Indicator height for default MoonTabBar.", + enabled: false, + initial: 2, + max: 4, + ); + + final gapKnob = context.knobs.nullable.sliderInt( + label: "gap", + description: "Gap between MoonTabBar children.", + enabled: false, + initial: 4, + max: 12, + ); + + final showLeadingKnob = context.knobs.boolean( + label: "leading", + description: "Show widget in MoonTabBar leading slot.", + ); + + final showLabelKnob = context.knobs.boolean( + label: "label", + description: "Show widget in MoonTabBar label slot.", + initial: true, + ); + + final showTrailingKnob = context.knobs.boolean( + label: "trailing", + description: "Show widget in MoonTabBar trailing slot.", + ); + + final isExpandedKnob = context.knobs.boolean( + label: "isExpanded", + description: "Expand MoonTabBar horizontally.", + ); + + final tabStyle = MoonTabStyle( + textColor: textColor, + selectedTextColor: selectedTextColor, + indicatorColor: indicatorColor, + indicatorHeight: indicatorHeightKnob?.toDouble(), + ); + + final pillTabStyle = MoonPillTabStyle( + textColor: textColor, + selectedTextColor: selectedTextColor, + selectedTabColor: selectedTabColor, + borderRadius: borderRadiusKnob != null ? BorderRadius.circular(borderRadiusKnob.toDouble()) : null, + ); + + return Center( + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: 64), + const TextDivider(text: "Default MoonTabBar"), + const SizedBox(height: 32), + Column( + children: [ + MoonTabBar( + tabBarSize: tabsSizesKnob, + isExpanded: isExpandedKnob, + gap: gapKnob?.toDouble(), + tabs: [ + MoonTab( + leading: showLeadingKnob ? const MoonIcon(MoonIcons.frame_24) : null, + label: showLabelKnob ? const Text('Tab1') : null, + trailing: showTrailingKnob ? const MoonIcon(MoonIcons.frame_24) : null, + tabStyle: tabStyle, + ), + MoonTab( + leading: showLeadingKnob ? const MoonIcon(MoonIcons.frame_24) : null, + label: showLabelKnob ? const Text('Tab2') : null, + trailing: showTrailingKnob ? const MoonIcon(MoonIcons.frame_24) : null, + tabStyle: tabStyle, + ), + MoonTab( + leading: showLeadingKnob ? const MoonIcon(MoonIcons.frame_24) : null, + label: showLabelKnob ? const Text('Tab3') : null, + trailing: showTrailingKnob ? const MoonIcon(MoonIcons.frame_24) : null, + tabStyle: tabStyle, + ), + ], + ), + const SizedBox(height: 32), + const TextDivider(text: "MoonTabBar with disabled tab"), + const SizedBox(height: 32), + MoonTabBar( + tabBarSize: tabsSizesKnob, + isExpanded: isExpandedKnob, + gap: gapKnob?.toDouble(), + tabs: [ + MoonTab( + trailing: const MoonIcon(MoonIcons.frame_24), + tabStyle: tabStyle, + ), + MoonTab( + trailing: const MoonIcon(MoonIcons.frame_24), + tabStyle: tabStyle, + disabled: true, + ), + MoonTab( + trailing: const MoonIcon(MoonIcons.frame_24), + tabStyle: tabStyle, + ), + MoonTab( + trailing: const MoonIcon(MoonIcons.frame_24), + tabStyle: tabStyle, + ), + ], + ), + const SizedBox(height: 32), + const TextDivider(text: "Pill MoonTabBar"), + const SizedBox(height: 32), + MoonTabBar.pill( + tabBarSize: tabsSizesKnob, + isExpanded: isExpandedKnob, + gap: gapKnob?.toDouble(), + pillTabs: [ + MoonPillTab( + leading: showLeadingKnob ? const MoonIcon(MoonIcons.frame_24) : null, + label: showLabelKnob ? const Text('Tab1') : null, + trailing: showTrailingKnob ? const MoonIcon(MoonIcons.frame_24) : null, + tabStyle: pillTabStyle, + ), + MoonPillTab( + leading: showLeadingKnob ? const MoonIcon(MoonIcons.frame_24) : null, + label: showLabelKnob ? const Text('Tab2') : null, + trailing: showTrailingKnob ? const MoonIcon(MoonIcons.frame_24) : null, + tabStyle: pillTabStyle, + ), + MoonPillTab( + leading: showLeadingKnob ? const MoonIcon(MoonIcons.frame_24) : null, + label: showLabelKnob ? const Text('Tab3') : null, + trailing: showTrailingKnob ? const MoonIcon(MoonIcons.frame_24) : null, + tabStyle: pillTabStyle, + ), + ], + ), + ], + ), + const SizedBox(height: 64), + ], + ), + ), + ); + }, + ); +} diff --git a/example/lib/src/storybook/storybook.dart b/example/lib/src/storybook/storybook.dart index 27c288d3..98855939 100644 --- a/example/lib/src/storybook/storybook.dart +++ b/example/lib/src/storybook/storybook.dart @@ -16,6 +16,7 @@ import 'package:example/src/storybook/stories/popover.dart'; import 'package:example/src/storybook/stories/radio.dart'; import 'package:example/src/storybook/stories/segmented_control.dart'; import 'package:example/src/storybook/stories/switch.dart'; +import 'package:example/src/storybook/stories/tab_bar.dart'; import 'package:example/src/storybook/stories/tag.dart'; import 'package:example/src/storybook/stories/text_area.dart'; import 'package:example/src/storybook/stories/text_input.dart'; @@ -96,6 +97,7 @@ class StorybookPage extends StatelessWidget { RadioStory(), SegmentedControlStory(), SwitchStory(), + TabBarStory(), TagStory(), TextAreaStory(), TextInputStory(), diff --git a/lib/moon_design.dart b/lib/moon_design.dart index 3d16a4da..a36335f7 100644 --- a/lib/moon_design.dart +++ b/lib/moon_design.dart @@ -69,8 +69,15 @@ export 'package:moon_design/src/widgets/popover/popover.dart'; export 'package:moon_design/src/widgets/progress/circular_progress.dart'; export 'package:moon_design/src/widgets/progress/linear_progress.dart'; export 'package:moon_design/src/widgets/radio/radio.dart'; +export 'package:moon_design/src/widgets/segmented_control/segment.dart'; +export 'package:moon_design/src/widgets/segmented_control/segment_style.dart'; export 'package:moon_design/src/widgets/segmented_control/segmented_control.dart'; export 'package:moon_design/src/widgets/switch/switch.dart'; +export 'package:moon_design/src/widgets/tab_bar/pill_tab.dart'; +export 'package:moon_design/src/widgets/tab_bar/pill_tab_style.dart'; +export 'package:moon_design/src/widgets/tab_bar/tab.dart'; +export 'package:moon_design/src/widgets/tab_bar/tab_bar.dart'; +export 'package:moon_design/src/widgets/tab_bar/tab_style.dart'; export 'package:moon_design/src/widgets/tag/tag.dart'; export 'package:moon_design/src/widgets/text_area/text_area.dart'; export 'package:moon_design/src/widgets/text_input/form_text_input.dart'; diff --git a/lib/src/theme/segmented_control/segmented_control_colors.dart b/lib/src/theme/segmented_control/segmented_control_colors.dart index 69ef2e02..94ff772c 100644 --- a/lib/src/theme/segmented_control/segmented_control_colors.dart +++ b/lib/src/theme/segmented_control/segmented_control_colors.dart @@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:moon_design/src/theme/colors.dart'; +import 'package:moon_design/src/theme/typography/typography.dart'; import 'package:moon_design/src/utils/color_premul_lerp.dart'; @immutable @@ -9,14 +10,14 @@ class MoonSegmentedControlColors extends ThemeExtension with DiagnosticableTreeMixin { + static final light = MoonTabBarColors( + indicatorColor: MoonColors.light.piccolo, + textColor: MoonTypography.light.colors.bodyPrimary, + selectedTextColor: MoonColors.light.piccolo, + selectedPillTabColor: MoonColors.light.gohan, + ); + + static final dark = MoonTabBarColors( + indicatorColor: MoonColors.dark.piccolo, + textColor: MoonTypography.dark.colors.bodyPrimary, + selectedTextColor: MoonColors.dark.piccolo, + selectedPillTabColor: MoonColors.dark.gohan, + ); + + /// TabBar tab indicator color. + final Color indicatorColor; + + /// TabBar default text color. + final Color textColor; + + /// TabBar selected tab text color. + final Color selectedTextColor; + + /// TabBar selected pill tab color. + final Color selectedPillTabColor; + + const MoonTabBarColors({ + required this.indicatorColor, + required this.textColor, + required this.selectedTextColor, + required this.selectedPillTabColor, + }); + + @override + MoonTabBarColors copyWith({ + Color? indicatorColor, + Color? textColor, + Color? selectedTextColor, + Color? selectedPillTabColor, + }) { + return MoonTabBarColors( + indicatorColor: indicatorColor ?? this.indicatorColor, + textColor: textColor ?? this.textColor, + selectedTextColor: selectedTextColor ?? this.selectedTextColor, + selectedPillTabColor: selectedPillTabColor ?? this.selectedPillTabColor, + ); + } + + @override + MoonTabBarColors lerp(ThemeExtension? other, double t) { + if (other is! MoonTabBarColors) return this; + + return MoonTabBarColors( + indicatorColor: colorPremulLerp(indicatorColor, other.indicatorColor, t)!, + textColor: colorPremulLerp(textColor, other.textColor, t)!, + selectedTextColor: colorPremulLerp(selectedTextColor, other.selectedTextColor, t)!, + selectedPillTabColor: colorPremulLerp(selectedPillTabColor, other.selectedPillTabColor, t)!, + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty("type", "MoonTabBarColors")) + ..add(ColorProperty("indicatorColor", indicatorColor)) + ..add(ColorProperty("textColor", textColor)) + ..add(ColorProperty("selectedTextColor", selectedTextColor)) + ..add(ColorProperty("selectedPillTabColor", selectedPillTabColor)); + } +} diff --git a/lib/src/theme/tab_bar/tab_bar_properties.dart b/lib/src/theme/tab_bar/tab_bar_properties.dart new file mode 100644 index 00000000..0b61ad7f --- /dev/null +++ b/lib/src/theme/tab_bar/tab_bar_properties.dart @@ -0,0 +1,64 @@ +import 'dart:ui'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'package:moon_design/moon_design.dart'; + +@immutable +class MoonTabBarProperties extends ThemeExtension with DiagnosticableTreeMixin { + static final properties = MoonTabBarProperties( + gap: MoonSizes.sizes.x5s, + transitionDuration: const Duration(milliseconds: 200), + transitionCurve: Curves.easeInOutCubic, + ); + + /// Gap between TabBar children. + final double gap; + + /// TabBar transition duration. + final Duration transitionDuration; + + /// TabBar transition curve. + final Curve transitionCurve; + + const MoonTabBarProperties({ + required this.gap, + required this.transitionDuration, + required this.transitionCurve, + }); + + @override + MoonTabBarProperties copyWith({ + double? gap, + Duration? transitionDuration, + Curve? transitionCurve, + }) { + return MoonTabBarProperties( + gap: gap ?? this.gap, + transitionDuration: transitionDuration ?? this.transitionDuration, + transitionCurve: transitionCurve ?? this.transitionCurve, + ); + } + + @override + MoonTabBarProperties lerp(ThemeExtension? other, double t) { + if (other is! MoonTabBarProperties) return this; + + return MoonTabBarProperties( + gap: lerpDouble(gap, other.gap, t)!, + transitionDuration: lerpDuration(transitionDuration, other.transitionDuration, t), + transitionCurve: other.transitionCurve, + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty("type", "MoonTabBarProperties")) + ..add(DoubleProperty("gap", gap)) + ..add(DiagnosticsProperty("transitionDuration", transitionDuration)) + ..add(DiagnosticsProperty("transitionCurve", transitionCurve)); + } +} diff --git a/lib/src/theme/tab_bar/tab_bar_size_properties.dart b/lib/src/theme/tab_bar/tab_bar_size_properties.dart new file mode 100644 index 00000000..c0390cfd --- /dev/null +++ b/lib/src/theme/tab_bar/tab_bar_size_properties.dart @@ -0,0 +1,112 @@ +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/typography/text_styles.dart'; + +@immutable +class MoonTabBarSizeProperties extends ThemeExtension with DiagnosticableTreeMixin { + static final sm = MoonTabBarSizeProperties( + borderRadius: MoonBorders.borders.interactiveXs, + tabGap: MoonSizes.sizes.x5s, + height: MoonSizes.sizes.sm, + iconSizeValue: MoonSizes.sizes.xs, + indicatorHeight: MoonSizes.sizes.x6s, + tabPadding: EdgeInsets.symmetric(horizontal: MoonSizes.sizes.x3s), + textStyle: MoonTextStyles.heading.text14, + ); + + static final md = MoonTabBarSizeProperties( + borderRadius: MoonBorders.borders.interactiveSm, + tabGap: MoonSizes.sizes.x4s, + height: MoonSizes.sizes.md, + iconSizeValue: MoonSizes.sizes.xs, + indicatorHeight: MoonSizes.sizes.x6s, + tabPadding: EdgeInsets.symmetric(horizontal: MoonSizes.sizes.x2s), + textStyle: MoonTextStyles.heading.text14, + ); + + /// TabBar pill tab border radius. + final BorderRadiusGeometry borderRadius; + + /// TabBar height. + final double height; + + /// TabBar tab icon size value. + final double iconSizeValue; + + /// TabBar tab indicator height. + final double indicatorHeight; + + /// Gap between leading, label and trailing widgets of tab. + final double tabGap; + + /// TabBar tab padding. + final EdgeInsetsGeometry tabPadding; + + /// TabBar default text style. + final TextStyle textStyle; + + const MoonTabBarSizeProperties({ + required this.borderRadius, + required this.height, + required this.iconSizeValue, + required this.indicatorHeight, + required this.tabGap, + required this.tabPadding, + required this.textStyle, + }); + + @override + MoonTabBarSizeProperties copyWith({ + BorderRadiusGeometry? borderRadius, + double? height, + double? iconSizeValue, + double? indicatorHeight, + double? tabGap, + EdgeInsetsGeometry? tabPadding, + TextStyle? textStyle, + }) { + return MoonTabBarSizeProperties( + borderRadius: borderRadius ?? this.borderRadius, + height: height ?? this.height, + iconSizeValue: iconSizeValue ?? this.iconSizeValue, + indicatorHeight: indicatorHeight ?? this.indicatorHeight, + tabGap: tabGap ?? this.tabGap, + tabPadding: tabPadding ?? this.tabPadding, + textStyle: textStyle ?? this.textStyle, + ); + } + + @override + MoonTabBarSizeProperties lerp(ThemeExtension? other, double t) { + if (other is! MoonTabBarSizeProperties) return this; + + return MoonTabBarSizeProperties( + borderRadius: BorderRadiusGeometry.lerp(borderRadius, other.borderRadius, t)!, + height: lerpDouble(height, other.height, t)!, + iconSizeValue: lerpDouble(iconSizeValue, other.iconSizeValue, t)!, + indicatorHeight: lerpDouble(indicatorHeight, other.indicatorHeight, t)!, + tabGap: lerpDouble(tabGap, other.tabGap, t)!, + tabPadding: EdgeInsetsGeometry.lerp(tabPadding, other.tabPadding, t)!, + textStyle: TextStyle.lerp(textStyle, other.textStyle, t)!, + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty("type", "MoonTabBarSizeProperties")) + ..add(DiagnosticsProperty("borderRadius", borderRadius)) + ..add(DoubleProperty("height", height)) + ..add(DoubleProperty("iconSizeValue", iconSizeValue)) + ..add(DoubleProperty("indicatorHeight", indicatorHeight)) + ..add(DoubleProperty("tabGap", tabGap)) + ..add(DiagnosticsProperty("tabPadding", tabPadding)) + ..add(DiagnosticsProperty("textStyle", textStyle)); + } +} diff --git a/lib/src/theme/tab_bar/tab_bar_sizes.dart b/lib/src/theme/tab_bar/tab_bar_sizes.dart new file mode 100644 index 00000000..32e3f8db --- /dev/null +++ b/lib/src/theme/tab_bar/tab_bar_sizes.dart @@ -0,0 +1,53 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'package:moon_design/src/theme/tab_bar/tab_bar_size_properties.dart'; + +@immutable +class MoonTabBarSizes extends ThemeExtension with DiagnosticableTreeMixin { + static final sizes = MoonTabBarSizes( + sm: MoonTabBarSizeProperties.sm, + md: MoonTabBarSizeProperties.md, + ); + + /// Small TabBar item properties. + final MoonTabBarSizeProperties sm; + + /// Medium TabBar item properties. + final MoonTabBarSizeProperties md; + + const MoonTabBarSizes({ + required this.sm, + required this.md, + }); + + @override + MoonTabBarSizes copyWith({ + MoonTabBarSizeProperties? sm, + MoonTabBarSizeProperties? md, + }) { + return MoonTabBarSizes( + sm: sm ?? this.sm, + md: md ?? this.md, + ); + } + + @override + MoonTabBarSizes lerp(ThemeExtension? other, double t) { + if (other is! MoonTabBarSizes) return this; + + return MoonTabBarSizes( + sm: sm.lerp(other.sm, t), + md: md.lerp(other.md, t), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty("type", "MoonTabBarSizes")) + ..add(DiagnosticsProperty("sm", sm)) + ..add(DiagnosticsProperty("md", md)); + } +} diff --git a/lib/src/theme/tab_bar/tab_bar_theme.dart b/lib/src/theme/tab_bar/tab_bar_theme.dart new file mode 100644 index 00000000..3dcad53b --- /dev/null +++ b/lib/src/theme/tab_bar/tab_bar_theme.dart @@ -0,0 +1,70 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'package:moon_design/src/theme/tab_bar/tab_bar_colors.dart'; +import 'package:moon_design/src/theme/tab_bar/tab_bar_properties.dart'; +import 'package:moon_design/src/theme/tab_bar/tab_bar_sizes.dart'; + +@immutable +class MoonTabBarTheme extends ThemeExtension with DiagnosticableTreeMixin { + static final light = MoonTabBarTheme( + colors: MoonTabBarColors.light, + properties: MoonTabBarProperties.properties, + sizes: MoonTabBarSizes.sizes, + ); + + static final dark = MoonTabBarTheme( + colors: MoonTabBarColors.dark, + properties: MoonTabBarProperties.properties, + sizes: MoonTabBarSizes.sizes, + ); + + /// TabBar colors. + final MoonTabBarColors colors; + + /// TabBar properties. + final MoonTabBarProperties properties; + + /// TabBar sizes. + final MoonTabBarSizes sizes; + + const MoonTabBarTheme({ + required this.colors, + required this.properties, + required this.sizes, + }); + + @override + MoonTabBarTheme copyWith({ + MoonTabBarColors? colors, + MoonTabBarProperties? properties, + MoonTabBarSizes? sizes, + }) { + return MoonTabBarTheme( + colors: colors ?? this.colors, + properties: properties ?? this.properties, + sizes: sizes ?? this.sizes, + ); + } + + @override + MoonTabBarTheme lerp(ThemeExtension? other, double t) { + if (other is! MoonTabBarTheme) return this; + + return MoonTabBarTheme( + colors: colors.lerp(other.colors, t), + properties: properties.lerp(other.properties, t), + sizes: sizes.lerp(other.sizes, t), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder diagnosticProperties) { + super.debugFillProperties(diagnosticProperties); + diagnosticProperties + ..add(DiagnosticsProperty("type", "MoonTabBarTheme")) + ..add(DiagnosticsProperty("colors", colors)) + ..add(DiagnosticsProperty("properties", properties)) + ..add(DiagnosticsProperty("sizes", sizes)); + } +} diff --git a/lib/src/theme/theme.dart b/lib/src/theme/theme.dart index 2d176b1b..11c1da2c 100644 --- a/lib/src/theme/theme.dart +++ b/lib/src/theme/theme.dart @@ -23,6 +23,7 @@ import 'package:moon_design/src/theme/segmented_control/segmented_control_theme. import 'package:moon_design/src/theme/shadows.dart'; import 'package:moon_design/src/theme/sizes.dart'; import 'package:moon_design/src/theme/switch/switch_theme.dart'; +import 'package:moon_design/src/theme/tab_bar/tab_bar_theme.dart'; import 'package:moon_design/src/theme/tag/tag_theme.dart'; import 'package:moon_design/src/theme/text_area/text_area_theme.dart'; import 'package:moon_design/src/theme/text_input/text_input_theme.dart'; @@ -55,6 +56,7 @@ class MoonTheme extends ThemeExtension with DiagnosticableTreeMixin { shadows: MoonShadows.light, sizes: MoonSizes.sizes, switchTheme: MoonSwitchTheme.light, + tabBarTheme: MoonTabBarTheme.light, tagTheme: MoonTagTheme.light, textAreaTheme: MoonTextAreaTheme.light, textInputTheme: MoonTextInputTheme.light, @@ -86,6 +88,7 @@ class MoonTheme extends ThemeExtension with DiagnosticableTreeMixin { shadows: MoonShadows.dark, sizes: MoonSizes.sizes, switchTheme: MoonSwitchTheme.dark, + tabBarTheme: MoonTabBarTheme.dark, tagTheme: MoonTagTheme.dark, textAreaTheme: MoonTextAreaTheme.dark, textInputTheme: MoonTextInputTheme.dark, @@ -160,6 +163,9 @@ class MoonTheme extends ThemeExtension with DiagnosticableTreeMixin { /// Moon Design System MoonSwitch widget theming. final MoonSwitchTheme switchTheme; + /// Moon Design System MoonTabBar widget theming. + final MoonTabBarTheme tabBarTheme; + /// Moon Design System MoonTag widget theming. final MoonTagTheme tagTheme; @@ -201,6 +207,7 @@ class MoonTheme extends ThemeExtension with DiagnosticableTreeMixin { required this.shadows, required this.sizes, required this.switchTheme, + required this.tabBarTheme, required this.tagTheme, required this.textAreaTheme, required this.textInputTheme, @@ -233,6 +240,7 @@ class MoonTheme extends ThemeExtension with DiagnosticableTreeMixin { MoonShadows? shadows, MoonSizes? sizes, MoonSwitchTheme? switchTheme, + MoonTabBarTheme? tabBarTheme, MoonTagTheme? tagTheme, MoonTextAreaTheme? textAreaTheme, MoonTextInputTheme? textInputTheme, @@ -263,6 +271,7 @@ class MoonTheme extends ThemeExtension with DiagnosticableTreeMixin { shadows: shadows ?? this.shadows, sizes: sizes ?? this.sizes, switchTheme: switchTheme ?? this.switchTheme, + tabBarTheme: tabBarTheme ?? this.tabBarTheme, tagTheme: tagTheme ?? this.tagTheme, textAreaTheme: textAreaTheme ?? this.textAreaTheme, textInputTheme: textInputTheme ?? this.textInputTheme, @@ -299,6 +308,7 @@ class MoonTheme extends ThemeExtension with DiagnosticableTreeMixin { shadows: shadows.lerp(other.shadows, t), sizes: sizes.lerp(other.sizes, t), switchTheme: switchTheme.lerp(other.switchTheme, t), + tabBarTheme: tabBarTheme.lerp(other.tabBarTheme, t), tagTheme: tagTheme.lerp(other.tagTheme, t), textAreaTheme: textAreaTheme.lerp(other.textAreaTheme, t), textInputTheme: textInputTheme.lerp(other.textInputTheme, t), @@ -335,6 +345,7 @@ class MoonTheme extends ThemeExtension with DiagnosticableTreeMixin { ..add(DiagnosticsProperty("MoonShadows", shadows)) ..add(DiagnosticsProperty("MoonSizes", sizes)) ..add(DiagnosticsProperty("MoonSwitchTheme", switchTheme)) + ..add(DiagnosticsProperty("MoonTabBarTheme", tabBarTheme)) ..add(DiagnosticsProperty("MoonTagTheme", tagTheme)) ..add(DiagnosticsProperty("MoonTextAreaTheme", textAreaTheme)) ..add(DiagnosticsProperty("MoonTextInputTheme", textInputTheme)) diff --git a/lib/src/widgets/common/base_segmented_tab_bar.dart b/lib/src/widgets/common/base_segmented_tab_bar.dart index 220ee2a6..b59fd89d 100644 --- a/lib/src/widgets/common/base_segmented_tab_bar.dart +++ b/lib/src/widgets/common/base_segmented_tab_bar.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; class BaseSegmentedTabBar extends StatefulWidget { final bool isExpanded; + final double gap; final int selectedIndex; final TabController? tabController; final ValueChanged valueChanged; @@ -10,6 +11,7 @@ class BaseSegmentedTabBar extends StatefulWidget { const BaseSegmentedTabBar({ super.key, required this.isExpanded, + required this.gap, required this.selectedIndex, this.tabController, required this.valueChanged, @@ -29,7 +31,14 @@ class _BaseSegmentedTabBarState extends State with SingleTi super.initState(); _tabKeys = widget.children.map((Widget tab) => GlobalKey()).toList(); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _controller = widget.tabController ?? + DefaultTabController.maybeOf(context) ?? TabController(length: widget.children.length, vsync: this, initialIndex: widget.selectedIndex); } @@ -63,18 +72,26 @@ class _BaseSegmentedTabBarState extends State with SingleTi return Row( mainAxisSize: MainAxisSize.min, children: List.generate( - widget.children.length, - (int index) => widget.isExpanded - ? Expanded( - child: Listener( - onPointerDown: (_) => _handleTap(index), - child: widget.children[index], - ), - ) - : Listener( - onPointerDown: (_) => _handleTap(index), - child: widget.children[index], - ), + widget.children.length * 2 - 1, + (int index) { + final int derivedIndex = index ~/ 2; + + return widget.isExpanded + ? index.isEven + ? Expanded( + child: Listener( + onPointerDown: (_) => _handleTap(derivedIndex), + child: widget.children[derivedIndex], + ), + ) + : SizedBox(width: widget.gap) + : index.isEven + ? Listener( + onPointerDown: (_) => _handleTap(derivedIndex), + child: widget.children[derivedIndex], + ) + : SizedBox(width: widget.gap); + }, ), ); } diff --git a/lib/src/widgets/segmented_control/segment.dart b/lib/src/widgets/segmented_control/segment.dart new file mode 100644 index 00000000..1563d320 --- /dev/null +++ b/lib/src/widgets/segmented_control/segment.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; + +import 'package:moon_design/src/widgets/segmented_control/segment_style.dart'; + +class Segment { + /// {@macro flutter.widgets.Focus.autofocus}. + final bool autoFocus; + + /// Whether this segment should be focusable. + final bool isFocusable; + + /// Whether this segment should show a focus effect. + final bool showFocusEffect; + + /// {@macro flutter.widgets.Focus.focusNode}. + final FocusNode? focusNode; + + /// The styling options for the segment. + final SegmentStyle? segmentStyle; + + /// The semantic label for the segment. + final String? semanticLabel; + + /// Callback that returns boolean value if segment is currently selected or not. + final ValueChanged? isSelected; + + /// The widget in the leading slot of the segment. + final Widget? leading; + + /// The widget in the label slot of the segment. + final Widget? label; + + /// The widget in the trailing slot of the segment. + final Widget? trailing; + + const Segment({ + this.autoFocus = false, + this.isFocusable = true, + this.showFocusEffect = true, + this.focusNode, + this.segmentStyle, + this.semanticLabel, + this.isSelected, + this.leading, + this.label, + this.trailing, + }); +} diff --git a/lib/src/widgets/segmented_control/segment_style.dart b/lib/src/widgets/segmented_control/segment_style.dart new file mode 100644 index 00000000..b50fc769 --- /dev/null +++ b/lib/src/widgets/segmented_control/segment_style.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; + +class SegmentStyle { + /// The border radius of segment. + final BorderRadiusGeometry? segmentBorderRadius; + + /// The color of the focus effect. + final Color? focusEffectColor; + + /// The color of the selected segment. + final Color? selectedSegmentColor; + + /// The default text color of segments. + final Color? textColor; + + /// The text color of selected segment. + final Color? selectedTextColor; + + /// Custom decoration for the segment. + final Decoration? decoration; + + /// The gap between the leading, label and trailing widgets of segment. + final double? segmentGap; + + /// The padding of the segment. + final EdgeInsetsGeometry? segmentPadding; + + /// The text style of the segment. + /// + /// If [TextStyle] color is used, then it overrides the [textColor] and [selectedTextColor]. + final TextStyle? textStyle; + + const SegmentStyle({ + this.segmentBorderRadius, + this.focusEffectColor, + this.selectedSegmentColor, + this.textColor, + this.selectedTextColor, + this.decoration, + this.segmentGap, + this.segmentPadding, + this.textStyle, + }); +} diff --git a/lib/src/widgets/segmented_control/segmented_control.dart b/lib/src/widgets/segmented_control/segmented_control.dart index f4bd554d..4bbdcb73 100644 --- a/lib/src/widgets/segmented_control/segmented_control.dart +++ b/lib/src/widgets/segmented_control/segmented_control.dart @@ -2,15 +2,19 @@ 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/opacity.dart'; import 'package:moon_design/src/theme/segmented_control/segmented_control_size_properties.dart'; import 'package:moon_design/src/theme/sizes.dart'; import 'package:moon_design/src/theme/theme.dart'; +import 'package:moon_design/src/theme/typography/typography.dart'; import 'package:moon_design/src/utils/extensions.dart'; import 'package:moon_design/src/utils/shape_decoration_premul.dart'; import 'package:moon_design/src/utils/squircle/squircle_border.dart'; import 'package:moon_design/src/widgets/common/animated_icon_theme.dart'; import 'package:moon_design/src/widgets/common/base_control.dart'; import 'package:moon_design/src/widgets/common/base_segmented_tab_bar.dart'; +import 'package:moon_design/src/widgets/segmented_control/segment.dart'; +import 'package:moon_design/src/widgets/segmented_control/segment_style.dart'; enum MoonSegmentedControlSize { sm, @@ -20,7 +24,10 @@ enum MoonSegmentedControlSize { typedef MoonCustomSegmentBuilder = Widget Function(BuildContext context, bool isSelected); class MoonSegmentedControl extends StatefulWidget { - /// Whether MoonSegmentedControl is expanded and takes up all available space horizontally. + /// Controls whether MoonSegmentedControl is disabled. + final bool isDisabled; + + /// Controls whether MoonSegmentedControl is expanded and takes up all available space horizontally. final bool isExpanded; /// The border radius of MoonSegmentedControl. @@ -38,10 +45,10 @@ class MoonSegmentedControl extends StatefulWidget { /// The width of the MoonSegmentedControl. final double? width; - /// MoonSegmentedControl transition duration. + /// The transition duration of MoonSegmentedControl. final Duration? transitionDuration; - /// MoonSegmentedControl transition curve. + /// The transition curve of MoonSegmentedControl. final Curve? transitionCurve; /// The padding of the MoonSegmentedControl. @@ -59,7 +66,7 @@ class MoonSegmentedControl extends StatefulWidget { /// Controller of MoonSegmentedControl selection and animation state. final TabController? tabController; - /// Returns current selected segment index. + /// Callback that returns current selected segment index. final ValueChanged? onSegmentChanged; /// The children of MoonSegmentedControl. At least one child is required. @@ -71,6 +78,7 @@ class MoonSegmentedControl extends StatefulWidget { /// MDS SegmentedControl widget. const MoonSegmentedControl({ super.key, + this.isDisabled = false, this.isExpanded = false, this.borderRadius, this.backgroundColor, @@ -93,6 +101,7 @@ class MoonSegmentedControl extends StatefulWidget { /// MDS custom SegmentedControl widget. const MoonSegmentedControl.custom({ super.key, + this.isDisabled = false, this.isExpanded = false, this.borderRadius, this.backgroundColor, @@ -120,11 +129,18 @@ class _MoonSegmentedControlState extends State { late final bool _hasDefaultSegments = widget.segments != null; late int _selectedIndex = widget.selectedIndex; - @override - void initState() { - super.initState(); - - _updateSegmentsSelectedStatus(); + MoonSegmentedControlSizeProperties _getMoonSegmentedControlSize( + BuildContext context, + MoonSegmentedControlSize? segmentedControlSize, + ) { + switch (segmentedControlSize) { + case MoonSegmentedControlSize.sm: + return context.moonTheme?.segmentedControlTheme.sizes.sm ?? MoonSegmentedControlSizeProperties.sm; + case MoonSegmentedControlSize.md: + return context.moonTheme?.segmentedControlTheme.sizes.md ?? MoonSegmentedControlSizeProperties.md; + default: + return context.moonTheme?.segmentedControlTheme.sizes.md ?? MoonSegmentedControlSizeProperties.md; + } } void _updateSegmentsSelectedStatus() { @@ -139,18 +155,11 @@ class _MoonSegmentedControlState extends State { } } - MoonSegmentedControlSizeProperties _getMoonSegmentedControlSize( - BuildContext context, - MoonSegmentedControlSize? segmentedControlSize, - ) { - switch (segmentedControlSize) { - case MoonSegmentedControlSize.sm: - return context.moonTheme?.segmentedControlTheme.sizes.sm ?? MoonSegmentedControlSizeProperties.sm; - case MoonSegmentedControlSize.md: - return context.moonTheme?.segmentedControlTheme.sizes.md ?? MoonSegmentedControlSizeProperties.md; - default: - return context.moonTheme?.segmentedControlTheme.sizes.md ?? MoonSegmentedControlSizeProperties.md; - } + @override + void initState() { + super.initState(); + + _updateSegmentsSelectedStatus(); } @override @@ -166,16 +175,13 @@ class _MoonSegmentedControlState extends State { context.moonTheme?.segmentedControlTheme.colors.backgroundColor ?? MoonColors.light.goku; + final double effectiveDisabledOpacityValue = context.moonOpacity?.disabled ?? MoonOpacity.opacities.disabled; + final double effectiveHeight = widget.height ?? effectiveMoonSegmentControlSize.height; final double effectiveGap = widget.gap ?? context.moonTheme?.segmentedControlTheme.properties.gap ?? MoonSizes.sizes.x5s; - final EdgeInsetsGeometry effectivePadding = - widget.padding ?? context.moonTheme?.segmentedControlTheme.properties.padding ?? const EdgeInsets.all(4); - - final EdgeInsets resolvedContentPadding = effectivePadding.resolve(Directionality.of(context)); - final Duration effectiveTransitionDuration = widget.transitionDuration ?? context.moonTheme?.segmentedControlTheme.properties.transitionDuration ?? const Duration(milliseconds: 200); @@ -184,62 +190,67 @@ class _MoonSegmentedControlState extends State { context.moonTheme?.segmentedControlTheme.properties.transitionCurve ?? Curves.easeInOutCubic; - return AnimatedContainer( - height: effectiveHeight, - width: widget.width, - padding: resolvedContentPadding, + final EdgeInsetsGeometry effectivePadding = + widget.padding ?? context.moonTheme?.segmentedControlTheme.properties.padding ?? const EdgeInsets.all(4); + + return AnimatedOpacity( + opacity: widget.isDisabled ? effectiveDisabledOpacityValue : 1, duration: effectiveTransitionDuration, - curve: effectiveTransitionCurve, - constraints: BoxConstraints(minWidth: effectiveHeight), - decoration: widget.decoration ?? - ShapeDecorationWithPremultipliedAlpha( - color: effectiveBackgroundColor, - shape: MoonSquircleBorder( - borderRadius: effectiveBorderRadius.squircleBorderRadius(context), + child: Container( + height: effectiveHeight, + width: widget.width, + padding: effectivePadding, + constraints: BoxConstraints(minWidth: effectiveHeight), + decoration: widget.decoration ?? + ShapeDecorationWithPremultipliedAlpha( + color: effectiveBackgroundColor, + shape: MoonSquircleBorder( + borderRadius: effectiveBorderRadius.squircleBorderRadius(context), + ), ), - ), - child: BaseSegmentedTabBar( - selectedIndex: widget.selectedIndex, - tabController: widget.tabController, - isExpanded: widget.isExpanded, - children: _hasDefaultSegments - ? List.generate( - widget.segments!.length, - (int index) { - return Padding( - padding: EdgeInsetsDirectional.only(start: index == 0 ? 0 : effectiveGap), - child: _SegmentBuilder( + child: BaseSegmentedTabBar( + gap: effectiveGap, + selectedIndex: widget.selectedIndex, + tabController: widget.tabController, + isExpanded: widget.isExpanded, + children: _hasDefaultSegments + ? List.generate( + widget.segments!.length, + (int index) { + return _SegmentBuilder( + isDisabled: widget.isDisabled, transitionDuration: effectiveTransitionDuration, transitionCurve: effectiveTransitionCurve, isSelected: index == _selectedIndex, backgroundColor: effectiveBackgroundColor, moonSegmentedControlSizeProperties: effectiveMoonSegmentControlSize, segment: widget.segments![index], - ), - ); - }, - ) - : List.generate( - widget.customSegments!.length, - (int index) { - return Padding( - padding: EdgeInsetsDirectional.only(start: index == 0 ? 0 : effectiveGap), - child: widget.customSegments![index](context, index == _selectedIndex), - ); - }, - ), - valueChanged: (int newIndex) { - widget.onSegmentChanged?.call(newIndex); - _updateSegmentsSelectedStatus(); + ); + }, + ) + : List.generate( + widget.customSegments!.length, + (int index) { + return widget.customSegments![index](context, index == _selectedIndex); + }, + ), + valueChanged: (int newIndex) { + if (_selectedIndex == newIndex) return; + if (widget.isDisabled) return; + + widget.onSegmentChanged?.call(newIndex); + _updateSegmentsSelectedStatus(); - setState(() => _selectedIndex = newIndex); - }, + setState(() => _selectedIndex = newIndex); + }, + ), ), ); } } class _SegmentBuilder extends StatelessWidget { + final bool isDisabled; final bool isSelected; final Color backgroundColor; final Duration transitionDuration; @@ -248,6 +259,7 @@ class _SegmentBuilder extends StatelessWidget { final Segment segment; const _SegmentBuilder({ + required this.isDisabled, required this.isSelected, required this.backgroundColor, required this.transitionDuration, @@ -256,17 +268,6 @@ class _SegmentBuilder extends StatelessWidget { required this.segment, }); - Color _getTextColor(BuildContext context, {required Color? textColor, required Color backgroundColor}) { - if (textColor != null) return textColor; - - final backgroundLuminance = backgroundColor.computeLuminance(); - if (backgroundLuminance > 0.5) { - return MoonColors.light.bulma; - } else { - return MoonColors.dark.bulma; - } - } - @override Widget build(BuildContext context) { final SegmentStyle? segmentStyle = segment.segmentStyle; @@ -278,6 +279,17 @@ class _SegmentBuilder extends StatelessWidget { context.moonTheme?.segmentedControlTheme.colors.selectedSegmentColor ?? MoonColors.light.gohan; + final Color effectiveTextColor = segmentStyle?.textStyle?.color ?? + segmentStyle?.textColor ?? + context.moonTheme?.segmentedControlTheme.colors.textColor ?? + MoonTypography.light.colors.bodyPrimary; + + final Color effectiveSelectedTextColor = segmentStyle?.selectedTextColor ?? + context.moonTheme?.segmentedControlTheme.colors.selectedTextColor ?? + MoonColors.light.piccolo; + + final TextStyle effectiveTextStyle = moonSegmentedControlSizeProperties.textStyle.merge(segmentStyle?.textStyle); + final double effectiveSegmentGap = segmentStyle?.segmentGap ?? moonSegmentedControlSizeProperties.segmentGap; final EdgeInsetsGeometry effectiveSegmentPadding = @@ -294,30 +306,19 @@ class _SegmentBuilder extends StatelessWidget { ) : resolvedDirectionalPadding; - final TextStyle effectiveTextStyle = moonSegmentedControlSizeProperties.textStyle; - return MoonBaseControl( - onLongPress: () => {}, - showScaleAnimation: false, - showFocusEffect: segment.showFocusEffect, + onLongPress: isDisabled ? null : () => {}, autofocus: segment.autoFocus, focusNode: segment.focusNode, isFocusable: segment.isFocusable, + showFocusEffect: segment.showFocusEffect, + focusEffectColor: segmentStyle?.focusEffectColor, + showScaleAnimation: false, semanticLabel: segment.semanticLabel, borderRadius: effectiveSegmentBorderRadius.squircleBorderRadius(context), cursor: isSelected ? SystemMouseCursors.basic : SystemMouseCursors.click, builder: (context, isEnabled, isHovered, isFocused, isPressed) { - final bool isActive = isSelected || isHovered || isPressed; - - final Color effectiveTextColor = _getTextColor( - context, - textColor: isActive ? segmentStyle?.selectedTextColor : segmentStyle?.textColor, - backgroundColor: isActive ? effectiveSelectedSegmentColor : backgroundColor, - ); - - final TextStyle resolvedTextStyle = effectiveTextStyle - .merge(segmentStyle?.textStyle) - .copyWith(color: segmentStyle?.textStyle?.color ?? effectiveTextColor); + final bool isActive = isEnabled && (isSelected || isHovered || isPressed); return AnimatedContainer( duration: transitionDuration, @@ -332,10 +333,10 @@ class _SegmentBuilder extends StatelessWidget { child: AnimatedIconTheme( size: moonSegmentedControlSizeProperties.iconSizeValue, duration: transitionDuration, - color: effectiveTextColor, + color: isActive ? effectiveSelectedTextColor : effectiveTextColor, child: AnimatedDefaultTextStyle( duration: transitionDuration, - style: resolvedTextStyle, + style: effectiveTextStyle.copyWith(color: isActive ? effectiveSelectedTextColor : effectiveTextColor), child: Center( child: Padding( padding: correctedSegmentPadding, @@ -365,87 +366,3 @@ class _SegmentBuilder extends StatelessWidget { ); } } - -class Segment { - /// {@macro flutter.widgets.Focus.autofocus}. - final bool autoFocus; - - /// Whether this segment should be focusable. - final bool isFocusable; - - /// Whether this segment should show a focus effect. - final bool showFocusEffect; - - /// {@macro flutter.widgets.Focus.focusNode}. - final FocusNode? focusNode; - - /// The semantic label for the segment. - final String? semanticLabel; - - /// The styling options for the segment. - final SegmentStyle? segmentStyle; - - /// Returns value if segment is currently selected or not. - final ValueChanged? isSelected; - - /// The widget in the leading slot of the segment. - final Widget? leading; - - /// The widget in the label slot of the segment. - final Widget? label; - - /// The widget in the trailing slot of the segment. - final Widget? trailing; - - const Segment({ - this.autoFocus = false, - this.isFocusable = true, - this.showFocusEffect = true, - this.focusNode, - this.semanticLabel, - this.segmentStyle, - this.isSelected, - this.leading, - this.label, - this.trailing, - }); -} - -class SegmentStyle { - /// The border radius of segment. - final BorderRadiusGeometry? segmentBorderRadius; - - /// The color of the selected segment. - final Color? selectedSegmentColor; - - /// The text color of unselected segments. - final Color? textColor; - - /// The text color of selected segment. - final Color? selectedTextColor; - - /// The gap between the leading, label and trailing widgets of segment. - final double? segmentGap; - - /// The padding of the segment. - final EdgeInsetsGeometry? segmentPadding; - - /// Custom decoration for the segment. - final Decoration? decoration; - - /// The text style of the segment. - /// - /// If [TextStyle] color is used, then it overrides the [textColor] and [selectedTextColor]. - final TextStyle? textStyle; - - const SegmentStyle({ - this.segmentBorderRadius, - this.selectedSegmentColor, - this.textColor, - this.selectedTextColor, - this.segmentGap, - this.segmentPadding, - this.decoration, - this.textStyle, - }); -} diff --git a/lib/src/widgets/tab_bar/pill_tab.dart b/lib/src/widgets/tab_bar/pill_tab.dart new file mode 100644 index 00000000..43435870 --- /dev/null +++ b/lib/src/widgets/tab_bar/pill_tab.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; + +import 'package:moon_design/src/widgets/tab_bar/pill_tab_style.dart'; + +class MoonPillTab { + /// Controls whether this tab is disabled. + final bool disabled; + + /// {@macro flutter.widgets.Focus.autofocus}. + final bool autoFocus; + + /// Whether this tab should be focusable. + final bool isFocusable; + + /// Whether this tab should show a focus effect. + final bool showFocusEffect; + + /// {@macro flutter.widgets.Focus.focusNode}. + final FocusNode? focusNode; + + /// The styling options for the tab. + final MoonPillTabStyle? tabStyle; + + /// The semantic label for the tab. + final String? semanticLabel; + + /// Callback that returns boolean value if tab is currently selected or not. + final ValueChanged? isSelected; + + /// The widget in the leading slot of the tab. + final Widget? leading; + + /// The widget in the label slot of the tab. + final Widget? label; + + /// The widget in the trailing slot of the tab. + final Widget? trailing; + + const MoonPillTab({ + this.disabled = false, + this.autoFocus = false, + this.isFocusable = true, + this.showFocusEffect = true, + this.focusNode, + this.tabStyle, + this.semanticLabel, + this.isSelected, + this.leading, + this.label, + this.trailing, + }); +} diff --git a/lib/src/widgets/tab_bar/pill_tab_style.dart b/lib/src/widgets/tab_bar/pill_tab_style.dart new file mode 100644 index 00000000..a8e390ad --- /dev/null +++ b/lib/src/widgets/tab_bar/pill_tab_style.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; + +class MoonPillTabStyle { + /// The border radius of tab. + final BorderRadiusGeometry? borderRadius; + + /// The color of the tab focus effect. + final Color? focusEffectColor; + + /// The color of the selected tab. + final Color? selectedTabColor; + + /// The default text color of the tab. + final Color? textColor; + + /// The text color of selected tab. + final Color? selectedTextColor; + + /// Custom decoration of the tab. + final Decoration? decoration; + + /// The gap between the leading, label and trailing widgets of tab. + final double? tabGap; + + /// The padding of the tab. + final EdgeInsetsGeometry? tabPadding; + + /// The text style of the tab. + /// + /// If [TextStyle] color is used, then it overrides the [textColor] and [selectedTextColor]. + final TextStyle? textStyle; + + const MoonPillTabStyle({ + this.borderRadius, + this.focusEffectColor, + this.selectedTabColor, + this.textColor, + this.selectedTextColor, + this.decoration, + this.tabGap, + this.tabPadding, + this.textStyle, + }); +} diff --git a/lib/src/widgets/tab_bar/tab.dart b/lib/src/widgets/tab_bar/tab.dart new file mode 100644 index 00000000..4553f2d1 --- /dev/null +++ b/lib/src/widgets/tab_bar/tab.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; + +import 'package:moon_design/src/widgets/tab_bar/tab_style.dart'; + +class MoonTab { + /// Controls whether this tab is disabled. + final bool disabled; + + /// {@macro flutter.widgets.Focus.autofocus}. + final bool autoFocus; + + /// Whether this tab should be focusable. + final bool isFocusable; + + /// Whether this tab should show a focus effect. + final bool showFocusEffect; + + /// {@macro flutter.widgets.Focus.focusNode}. + final FocusNode? focusNode; + + /// The styling options for the tab. + final MoonTabStyle? tabStyle; + + /// The semantic label for the tab. + final String? semanticLabel; + + /// Returns value if tab is currently selected or not. + final ValueChanged? isSelected; + + /// The widget in the leading slot of the tab. + final Widget? leading; + + /// The widget in the label slot of the tab. + final Widget? label; + + /// The widget in the trailing slot of the tab. + final Widget? trailing; + + const MoonTab({ + this.disabled = false, + this.autoFocus = false, + this.isFocusable = true, + this.showFocusEffect = true, + this.focusNode, + this.tabStyle, + this.semanticLabel, + this.isSelected, + this.leading, + this.label, + this.trailing, + }); +} diff --git a/lib/src/widgets/tab_bar/tab_bar.dart b/lib/src/widgets/tab_bar/tab_bar.dart new file mode 100644 index 00000000..d3e1dfa4 --- /dev/null +++ b/lib/src/widgets/tab_bar/tab_bar.dart @@ -0,0 +1,531 @@ +import 'package:flutter/material.dart'; + +import 'package:moon_design/src/theme/colors.dart'; +import 'package:moon_design/src/theme/sizes.dart'; +import 'package:moon_design/src/theme/tab_bar/tab_bar_size_properties.dart'; +import 'package:moon_design/src/theme/theme.dart'; +import 'package:moon_design/src/theme/typography/typography.dart'; +import 'package:moon_design/src/utils/extensions.dart'; +import 'package:moon_design/src/utils/shape_decoration_premul.dart'; +import 'package:moon_design/src/utils/squircle/squircle_border.dart'; +import 'package:moon_design/src/widgets/common/animated_icon_theme.dart'; +import 'package:moon_design/src/widgets/common/base_control.dart'; +import 'package:moon_design/src/widgets/common/base_segmented_tab_bar.dart'; +import 'package:moon_design/src/widgets/tab_bar/pill_tab.dart'; +import 'package:moon_design/src/widgets/tab_bar/pill_tab_style.dart'; +import 'package:moon_design/src/widgets/tab_bar/tab.dart'; +import 'package:moon_design/src/widgets/tab_bar/tab_style.dart'; + +enum MoonTabBarVariant { + indicator, + pill, + custom, +} + +enum MoonTabBarSize { + sm, + md, +} + +typedef MoonCustomTabBuilder = Widget Function(BuildContext context, bool isSelected); + +class MoonTabBar extends StatefulWidget { + /// Controls whether MoonTabBar is expanded and takes up all available space horizontally. + final bool isExpanded; + + /// The gap between MoonTabBar children. + final double? gap; + + /// The height of the MoonTabBar. + final double? height; + + /// The width of the MoonTabBar. + final double? width; + + /// Transition duration of MoonTabBar. + final Duration? transitionDuration; + + /// Transition curve of MoonTabBar. + final Curve? transitionCurve; + + /// The padding of the MoonTabBar. + final EdgeInsetsGeometry? padding; + + /// The index of initially selected tab. + final int selectedIndex; + + /// The size of the MoonTabBar. + final MoonTabBarSize? tabBarSize; + + /// Custom decoration of the MoonTabBar. + final Decoration? decoration; + + /// Controller of MoonTabBar selection and animation state. + final TabController? tabController; + + /// Callback that returns current selected tab index. + final ValueChanged? onTabChanged; + + /// The children of MoonTabBar. At least one child is required. + final List? tabs; + + /// The children of pill MoonTabBar. At least one child is required. + final List? pillTabs; + + /// The children of custom MoonTabBar. At least one child is required. + final List? customTabs; + + /// MDS TabBar widget. + const MoonTabBar({ + super.key, + this.isExpanded = false, + this.gap, + this.height, + this.width, + this.transitionDuration, + this.transitionCurve, + this.padding, + this.selectedIndex = 0, + this.tabBarSize, + this.decoration, + this.tabController, + this.onTabChanged, + required this.tabs, + }) : assert(height == null || height > 0), + assert(tabs != null && tabs.length > 0), + pillTabs = null, + customTabs = null; + + /// MDS pill TabBar widget. + const MoonTabBar.pill({ + super.key, + this.isExpanded = false, + this.gap, + this.height, + this.width, + this.transitionDuration, + this.transitionCurve, + this.padding, + this.selectedIndex = 0, + this.tabBarSize, + this.decoration, + this.tabController, + this.onTabChanged, + required this.pillTabs, + }) : assert(height == null || height > 0), + assert(pillTabs != null && pillTabs.length > 0), + tabs = null, + customTabs = null; + + /// MDS custom TabBar widget. + const MoonTabBar.custom({ + super.key, + this.isExpanded = false, + this.gap, + this.height, + this.width, + this.transitionDuration, + this.transitionCurve, + this.padding, + this.selectedIndex = 0, + this.tabBarSize, + this.decoration, + this.tabController, + this.onTabChanged, + required this.customTabs, + }) : assert(height == null || height > 0), + assert(customTabs != null && customTabs.length > 0), + tabs = null, + pillTabs = null; + + @override + State createState() => _MoonTabBarState(); +} + +class _MoonTabBarState extends State { + late int _selectedIndex = widget.selectedIndex; + late MoonTabBarVariant _tabBarVariant; + + late Duration _effectiveTransitionDuration; + late Curve _effectiveTransitionCurve; + late MoonTabBarSizeProperties _effectiveMoonTabBarSize; + + MoonTabBarSizeProperties _getMoonTabBarSize(BuildContext context, MoonTabBarSize? tabBarSize) { + switch (tabBarSize) { + case MoonTabBarSize.sm: + return context.moonTheme?.tabBarTheme.sizes.sm ?? MoonTabBarSizeProperties.sm; + case MoonTabBarSize.md: + return context.moonTheme?.tabBarTheme.sizes.md ?? MoonTabBarSizeProperties.md; + default: + return context.moonTheme?.tabBarTheme.sizes.md ?? MoonTabBarSizeProperties.md; + } + } + + void _setSelectedTabBarVariant() { + if (widget.tabs != null) { + _tabBarVariant = MoonTabBarVariant.indicator; + } else if (widget.pillTabs != null) { + _tabBarVariant = MoonTabBarVariant.pill; + } else { + _tabBarVariant = MoonTabBarVariant.custom; + } + } + + void _updateTabsSelectedStatus() { + if (_tabBarVariant == MoonTabBarVariant.indicator) { + widget.tabs?.asMap().forEach((int index, MoonTab tab) { + tab.isSelected?.call(index == _selectedIndex); + }); + } else if (_tabBarVariant == MoonTabBarVariant.pill) { + widget.pillTabs?.asMap().forEach((int index, MoonPillTab pillTab) { + pillTab.isSelected?.call(index == _selectedIndex); + }); + } else { + widget.customTabs?.asMap().forEach((int index, Widget Function(BuildContext, bool) customTab) { + customTab.call(context, index == _selectedIndex); + }); + } + } + + List _generateTabs() { + switch (_tabBarVariant) { + case MoonTabBarVariant.indicator: + return _generateIndicatorTabs(); + case MoonTabBarVariant.pill: + return _generatePillTabs(); + default: + return _generateCustomTabs(); + } + } + + @override + void initState() { + super.initState(); + + _setSelectedTabBarVariant(); + _updateTabsSelectedStatus(); + } + + List _generateIndicatorTabs() { + return List.generate( + widget.tabs!.length, + (int index) { + return _IndicatorTabBuilder( + transitionDuration: _effectiveTransitionDuration, + transitionCurve: _effectiveTransitionCurve, + isSelected: index == _selectedIndex, + moonTabBarSizeProperties: _effectiveMoonTabBarSize, + tab: widget.tabs![index], + ); + }, + ); + } + + List _generatePillTabs() { + return List.generate( + widget.pillTabs!.length, + (int index) { + return _PillTabBuilder( + transitionDuration: _effectiveTransitionDuration, + transitionCurve: _effectiveTransitionCurve, + isSelected: index == _selectedIndex, + moonTabBarSizeProperties: _effectiveMoonTabBarSize, + tab: widget.pillTabs![index], + ); + }, + ); + } + + List _generateCustomTabs() { + return List.generate( + widget.customTabs!.length, + (int index) { + return widget.customTabs![index](context, index == _selectedIndex); + }, + ); + } + + @override + Widget build(BuildContext context) { + _effectiveMoonTabBarSize = _getMoonTabBarSize(context, widget.tabBarSize); + + _effectiveTransitionDuration = widget.transitionDuration ?? + context.moonTheme?.tabBarTheme.properties.transitionDuration ?? + const Duration(milliseconds: 200); + + _effectiveTransitionCurve = + widget.transitionCurve ?? context.moonTheme?.tabBarTheme.properties.transitionCurve ?? Curves.easeInOutCubic; + + final double effectiveHeight = widget.height ?? _effectiveMoonTabBarSize.height; + + final double effectiveGap = widget.gap ?? context.moonTheme?.tabBarTheme.properties.gap ?? MoonSizes.sizes.x5s; + + return Container( + height: effectiveHeight, + width: widget.width, + padding: widget.padding, + decoration: widget.decoration, + constraints: BoxConstraints(minWidth: effectiveHeight), + child: BaseSegmentedTabBar( + gap: effectiveGap, + isExpanded: widget.isExpanded, + selectedIndex: widget.selectedIndex, + tabController: widget.tabController, + children: _generateTabs(), + valueChanged: (int newIndex) { + if (_selectedIndex == newIndex) return; + if (widget.tabs != null && widget.tabs![newIndex].disabled) return; + if (widget.pillTabs != null && widget.pillTabs![newIndex].disabled) return; + + widget.onTabChanged?.call(newIndex); + _updateTabsSelectedStatus(); + + setState(() => _selectedIndex = newIndex); + }, + ), + ); + } +} + +class _IndicatorTabBuilder extends StatelessWidget { + final bool isSelected; + final Duration transitionDuration; + final Curve transitionCurve; + final MoonTabBarSizeProperties moonTabBarSizeProperties; + final MoonTab tab; + + const _IndicatorTabBuilder({ + required this.isSelected, + required this.transitionDuration, + required this.transitionCurve, + required this.moonTabBarSizeProperties, + required this.tab, + }); + + @override + Widget build(BuildContext context) { + final MoonTabStyle? tabStyle = tab.tabStyle; + + final Color effectiveIndicatorColor = + tabStyle?.indicatorColor ?? context.moonTheme?.tabBarTheme.colors.indicatorColor ?? MoonColors.light.piccolo; + + final Color effectiveTextColor = tabStyle?.textStyle?.color ?? + tabStyle?.textColor ?? + context.moonTheme?.tabBarTheme.colors.textColor ?? + MoonTypography.light.colors.bodyPrimary; + + final Color effectiveSelectedTextColor = tabStyle?.selectedTextColor ?? + context.moonTheme?.tabBarTheme.colors.selectedTextColor ?? + MoonColors.light.piccolo; + + final TextStyle effectiveTextStyle = moonTabBarSizeProperties.textStyle.merge(tabStyle?.textStyle); + + final double effectiveIndicatorHeight = tabStyle?.indicatorHeight ?? moonTabBarSizeProperties.indicatorHeight; + + final double effectiveTabGap = tabStyle?.tabGap ?? moonTabBarSizeProperties.tabGap; + + final EdgeInsetsGeometry effectiveTabPadding = tabStyle?.tabPadding ?? moonTabBarSizeProperties.tabPadding; + + final EdgeInsets resolvedDirectionalPadding = effectiveTabPadding.resolve(Directionality.of(context)); + + final EdgeInsetsGeometry correctedTabPadding = tabStyle?.tabPadding == null + ? EdgeInsetsDirectional.fromSTEB( + tab.leading == null && tab.label != null ? resolvedDirectionalPadding.left : 0, + resolvedDirectionalPadding.top, + tab.trailing == null && tab.label != null ? resolvedDirectionalPadding.right : 0, + resolvedDirectionalPadding.bottom, + ) + : resolvedDirectionalPadding; + + return MoonBaseControl( + semanticLabel: tab.semanticLabel, + onLongPress: tab.disabled ? null : () => {}, + autofocus: tab.autoFocus, + focusNode: tab.focusNode, + isFocusable: tab.isFocusable, + showFocusEffect: tab.showFocusEffect, + focusEffectColor: tabStyle?.focusEffectColor, + showScaleAnimation: false, + cursor: isSelected ? SystemMouseCursors.basic : SystemMouseCursors.click, + builder: (context, isEnabled, isHovered, isFocused, isPressed) { + final bool isActive = isEnabled && (isSelected || isHovered || isPressed); + + return Container( + decoration: tabStyle?.decoration, + child: Stack( + children: [ + AnimatedIconTheme( + size: moonTabBarSizeProperties.iconSizeValue, + duration: transitionDuration, + color: isActive ? effectiveSelectedTextColor : effectiveTextColor, + child: AnimatedDefaultTextStyle( + duration: transitionDuration, + style: effectiveTextStyle.copyWith(color: isActive ? effectiveSelectedTextColor : effectiveTextColor), + child: Center( + child: Padding( + padding: correctedTabPadding, + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (tab.leading != null) + Padding( + padding: EdgeInsets.symmetric(horizontal: effectiveTabGap), + child: tab.leading, + ), + if (tab.label != null) + ConstrainedBox( + constraints: BoxConstraints(minHeight: moonTabBarSizeProperties.iconSizeValue), + child: Center(child: tab.label), + ), + if (tab.trailing != null) + Padding( + padding: EdgeInsets.symmetric(horizontal: effectiveTabGap), + child: tab.trailing, + ), + ], + ), + ), + ), + ), + ), + Positioned( + bottom: 0, + left: 0, + right: 0, + child: LayoutBuilder( + builder: (context, constraints) { + return Align( + alignment: Directionality.of(context) == TextDirection.ltr + ? Alignment.bottomLeft + : Alignment.bottomRight, + child: AnimatedContainer( + duration: transitionDuration, + color: effectiveIndicatorColor, + height: effectiveIndicatorHeight, + width: isActive ? constraints.maxWidth : 0, + ), + ); + }, + ), + ) + ], + ), + ); + }, + ); + } +} + +class _PillTabBuilder extends StatelessWidget { + final bool isSelected; + final Duration transitionDuration; + final Curve transitionCurve; + final MoonTabBarSizeProperties moonTabBarSizeProperties; + final MoonPillTab tab; + + const _PillTabBuilder({ + required this.isSelected, + required this.transitionDuration, + required this.transitionCurve, + required this.moonTabBarSizeProperties, + required this.tab, + }); + + @override + Widget build(BuildContext context) { + final MoonPillTabStyle? tabStyle = tab.tabStyle; + + final BorderRadiusGeometry effectiveTabBorderRadius = + tabStyle?.borderRadius ?? moonTabBarSizeProperties.borderRadius; + + final Color effectiveSelectedTabColor = tabStyle?.selectedTabColor ?? + context.moonTheme?.tabBarTheme.colors.selectedPillTabColor ?? + MoonColors.light.gohan; + + final Color effectiveTextColor = tabStyle?.textStyle?.color ?? + tabStyle?.textColor ?? + context.moonTheme?.tabBarTheme.colors.textColor ?? + MoonTypography.light.colors.bodyPrimary; + + final Color effectiveSelectedTextColor = tabStyle?.selectedTextColor ?? + context.moonTheme?.tabBarTheme.colors.selectedTextColor ?? + MoonColors.light.piccolo; + + final TextStyle effectiveTextStyle = moonTabBarSizeProperties.textStyle.merge(tabStyle?.textStyle); + + final double effectiveTabGap = tabStyle?.tabGap ?? moonTabBarSizeProperties.tabGap; + + final EdgeInsetsGeometry effectiveTabPadding = tabStyle?.tabPadding ?? moonTabBarSizeProperties.tabPadding; + + final EdgeInsets resolvedDirectionalPadding = effectiveTabPadding.resolve(Directionality.of(context)); + + final EdgeInsetsGeometry correctedTabPadding = tabStyle?.tabPadding == null + ? EdgeInsetsDirectional.fromSTEB( + tab.leading == null && tab.label != null ? resolvedDirectionalPadding.left : 0, + resolvedDirectionalPadding.top, + tab.trailing == null && tab.label != null ? resolvedDirectionalPadding.right : 0, + resolvedDirectionalPadding.bottom, + ) + : resolvedDirectionalPadding; + + return MoonBaseControl( + semanticLabel: tab.semanticLabel, + onLongPress: tab.disabled ? null : () => {}, + autofocus: tab.autoFocus, + focusNode: tab.focusNode, + isFocusable: tab.isFocusable, + showFocusEffect: tab.showFocusEffect, + focusEffectColor: tabStyle?.focusEffectColor, + showScaleAnimation: false, + borderRadius: effectiveTabBorderRadius.squircleBorderRadius(context), + cursor: isSelected ? SystemMouseCursors.basic : SystemMouseCursors.click, + builder: (context, isEnabled, isHovered, isFocused, isPressed) { + final bool isActive = isEnabled && (isSelected || isHovered || isPressed); + + return AnimatedContainer( + duration: transitionDuration, + curve: transitionCurve, + decoration: tabStyle?.decoration ?? + ShapeDecorationWithPremultipliedAlpha( + color: isActive ? effectiveSelectedTabColor : Colors.transparent, + shape: MoonSquircleBorder( + borderRadius: effectiveTabBorderRadius.squircleBorderRadius(context), + ), + ), + child: AnimatedIconTheme( + size: moonTabBarSizeProperties.iconSizeValue, + duration: transitionDuration, + color: isActive ? effectiveSelectedTextColor : effectiveTextColor, + child: AnimatedDefaultTextStyle( + duration: transitionDuration, + style: effectiveTextStyle.copyWith(color: isActive ? effectiveSelectedTextColor : effectiveTextColor), + child: Center( + child: Padding( + padding: correctedTabPadding, + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (tab.leading != null) + Padding( + padding: EdgeInsets.symmetric(horizontal: effectiveTabGap), + child: tab.leading, + ), + if (tab.label != null) tab.label!, + if (tab.trailing != null) + Padding( + padding: EdgeInsets.symmetric(horizontal: effectiveTabGap), + child: tab.trailing, + ), + ], + ), + ), + ), + ), + ), + ); + }, + ); + } +} diff --git a/lib/src/widgets/tab_bar/tab_style.dart b/lib/src/widgets/tab_bar/tab_style.dart new file mode 100644 index 00000000..74d9dac4 --- /dev/null +++ b/lib/src/widgets/tab_bar/tab_style.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; + +class MoonTabStyle { + /// The color of the focus effect. + final Color? focusEffectColor; + + /// The color of tab indicator. + final Color? indicatorColor; + + /// The default text color of the tab. + final Color? textColor; + + /// The text color of selected tab. + final Color? selectedTextColor; + + /// Custom decoration for the tab. + final Decoration? decoration; + + /// The height of the tab indicator. + final double? indicatorHeight; + + /// The gap between the leading, label and trailing widgets of tab. + final double? tabGap; + + /// The padding of the tab. + final EdgeInsetsGeometry? tabPadding; + + /// The text style of the tab. + /// + /// If [TextStyle] color is used, then it overrides the [textColor] and [selectedTextColor]. + final TextStyle? textStyle; + + const MoonTabStyle({ + this.focusEffectColor, + this.indicatorColor, + this.textColor, + this.selectedTextColor, + this.decoration, + this.indicatorHeight, + this.tabGap, + this.tabPadding, + this.textStyle, + }); +}