From 65b38f110b4aadd2ede37c9193b496ed1b74ed25 Mon Sep 17 00:00:00 2001 From: Joe Date: Thu, 27 Jun 2024 18:57:31 +0800 Subject: [PATCH] Implement nested header --- forui/example/lib/example.dart | 62 ++++++ forui/example/lib/main.dart | 78 ++----- forui/lib/forui.dart | 1 + forui/lib/src/theme/theme_data.dart | 10 + forui/lib/src/widgets/header/header.dart | 28 ++- .../lib/src/widgets/header/header_action.dart | 32 +-- .../widgets/nested_header/nested_header.dart | 202 ++++++++++++++++++ .../nested_header/nested_header_action.dart | 121 +++++++++++ 8 files changed, 438 insertions(+), 96 deletions(-) create mode 100644 forui/example/lib/example.dart create mode 100644 forui/lib/src/widgets/nested_header/nested_header.dart create mode 100644 forui/lib/src/widgets/nested_header/nested_header_action.dart diff --git a/forui/example/lib/example.dart b/forui/example/lib/example.dart new file mode 100644 index 000000000..643295ef5 --- /dev/null +++ b/forui/example/lib/example.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; + +import 'package:forui/forui.dart'; + +class Example extends StatefulWidget { + const Example({super.key}); + + @override + State createState() => _ExampleState(); +} + +class _ExampleState extends State { + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 10), + Expanded( + child: FTabs( + tabs: [ + FTabEntry( + label: 'Account', + content: FCard( + title: 'Account', + subtitle: 'Make changes to your account here. Click save when you are done.', + child: Column( + children: [ + Container( + color: Colors.red, + height: 100, + ), + ], + ), + ), + ), + FTabEntry( + label: 'Password', + content: FCard( + title: 'Password', + subtitle: 'Change your password here. After saving, you will be logged out.', + child: Column( + children: [ + Container( + color: Colors.blue, + height: 100, + ) + ], + ), + ), + ), + ], + ), + ), + ], + ); +} diff --git a/forui/example/lib/main.dart b/forui/example/lib/main.dart index ab9f066fa..22a3ebafe 100644 --- a/forui/example/lib/main.dart +++ b/forui/example/lib/main.dart @@ -2,6 +2,8 @@ import 'package:flutter/material.dart'; import 'package:forui/forui.dart'; +import 'package:forui_example/example.dart'; + void main() { runApp(const Application()); } @@ -15,72 +17,18 @@ class Application extends StatelessWidget { Widget build(BuildContext context) => MaterialApp( home: FTheme( data: FThemes.zinc.light, - child: const FScaffold( - header: FHeader(title: 'Example'), - content: ExampleWidget(), - ), - ), - ); -} - -class ExampleWidget extends StatefulWidget { - const ExampleWidget({super.key}); - - @override - State createState() => _ExampleWidgetState(); -} - -class _ExampleWidgetState extends State { - @override - void initState() { - super.initState(); - } - - @override - Widget build(BuildContext context) => Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 10), - Expanded( - child: FTabs( - tabs: [ - FTabEntry( - label: 'Account', - content: FCard( - title: 'Account', - subtitle: 'Make changes to your account here. Click save when you are done.', - child: Column( - children: [ - Container( - color: Colors.red, - height: 100, - ), - ], - ), - ), - ), - FTabEntry( - label: 'Password', - content: FCard( - title: 'Password', - subtitle: 'Change your password here. After saving, you will be logged out.', - child: Column( - children: [ - Container( - color: Colors.blue, - height: 100, - ) - ], - ), - ), - ), - ], - ), + child: FScaffold( + header: FHeader( + title: 'Example Example Example Example', + actions: [ + FHeaderAction( + icon: FAssets.icons.plus, + onPress: () {}, + ), + ], ), - ], + content: const Example(), + ), ), ); } diff --git a/forui/lib/forui.dart b/forui/lib/forui.dart index a9e3d7982..82ce85605 100644 --- a/forui/lib/forui.dart +++ b/forui/lib/forui.dart @@ -25,6 +25,7 @@ export 'src/widgets/button/button.dart' hide FButtonContent, Variant; export 'src/widgets/card/card.dart' hide FCardContent; export 'src/widgets/dialog/dialog.dart' hide FDialogContent, FHorizontalDialogContent, FVerticalDialogContent; export 'src/widgets/header/header.dart'; +export 'src/widgets/nested_header/nested_header.dart'; export 'src/widgets/tabs/tabs.dart'; export 'src/widgets/text_field/text_field.dart'; export 'src/widgets/scaffold.dart'; diff --git a/forui/lib/src/theme/theme_data.dart b/forui/lib/src/theme/theme_data.dart index c988abee1..33b37ff89 100644 --- a/forui/lib/src/theme/theme_data.dart +++ b/forui/lib/src/theme/theme_data.dart @@ -40,6 +40,9 @@ final class FThemeData with Diagnosticable { /// The header styles. final FHeaderStyle headerStyle; + /// The nested header styles. + final FNestedHeaderStyle nestedHeaderStyle; + /// The tabs styles. final FTabsStyle tabsStyle; @@ -68,6 +71,7 @@ final class FThemeData with Diagnosticable { required this.cardStyle, required this.dialogStyle, required this.headerStyle, + required this.nestedHeaderStyle, required this.tabsStyle, required this.textFieldStyle, required this.scaffoldStyle, @@ -94,6 +98,7 @@ final class FThemeData with Diagnosticable { cardStyle: FCardStyle.inherit(colorScheme: colorScheme, typography: typography, style: style), dialogStyle: FDialogStyle.inherit(colorScheme: colorScheme, typography: typography, style: style), headerStyle: FHeaderStyle.inherit(colorScheme: colorScheme, typography: typography, style: style), + nestedHeaderStyle: FNestedHeaderStyle.inherit(colorScheme: colorScheme, typography: typography, style: style), tabsStyle: FTabsStyle.inherit(colorScheme: colorScheme, typography: typography, style: style), textFieldStyle: FTextFieldStyle.inherit(colorScheme: colorScheme, typography: typography, style: style), scaffoldStyle: FScaffoldStyle.inherit(colorScheme: colorScheme, style: style), @@ -128,6 +133,7 @@ final class FThemeData with Diagnosticable { FCardStyle? cardStyle, FDialogStyle? dialogStyle, FHeaderStyle? headerStyle, + FNestedHeaderStyle? nestedHeaderStyle, FTabsStyle? tabsStyle, FTextFieldStyle? textFieldStyle, FScaffoldStyle? scaffoldStyle, @@ -143,6 +149,7 @@ final class FThemeData with Diagnosticable { cardStyle: cardStyle ?? this.cardStyle, dialogStyle: dialogStyle ?? this.dialogStyle, headerStyle: headerStyle ?? this.headerStyle, + nestedHeaderStyle: nestedHeaderStyle ?? this.nestedHeaderStyle, tabsStyle: tabsStyle ?? this.tabsStyle, textFieldStyle: textFieldStyle ?? this.textFieldStyle, scaffoldStyle: scaffoldStyle ?? this.scaffoldStyle, @@ -162,6 +169,7 @@ final class FThemeData with Diagnosticable { ..add(DiagnosticsProperty('cardStyle', cardStyle, level: DiagnosticLevel.debug)) ..add(DiagnosticsProperty('dialogStyle', dialogStyle, level: DiagnosticLevel.debug)) ..add(DiagnosticsProperty('headerStyle', headerStyle, level: DiagnosticLevel.debug)) + ..add(DiagnosticsProperty('nestedHeaderStyle', nestedHeaderStyle, level: DiagnosticLevel.debug)) ..add(DiagnosticsProperty('tabsStyle', tabsStyle, level: DiagnosticLevel.debug)) ..add(DiagnosticsProperty('textFieldStyle', textFieldStyle, level: DiagnosticLevel.debug)) ..add(DiagnosticsProperty('scaffoldStyle', scaffoldStyle, level: DiagnosticLevel.debug)) @@ -182,6 +190,7 @@ final class FThemeData with Diagnosticable { cardStyle == other.cardStyle && dialogStyle == other.dialogStyle && headerStyle == other.headerStyle && + nestedHeaderStyle == other.nestedHeaderStyle && tabsStyle == other.tabsStyle && textFieldStyle == other.textFieldStyle && scaffoldStyle == other.scaffoldStyle && @@ -198,6 +207,7 @@ final class FThemeData with Diagnosticable { cardStyle.hashCode ^ dialogStyle.hashCode ^ headerStyle.hashCode ^ + nestedHeaderStyle.hashCode ^ tabsStyle.hashCode ^ textFieldStyle.hashCode ^ scaffoldStyle.hashCode ^ diff --git a/forui/lib/src/widgets/header/header.dart b/forui/lib/src/widgets/header/header.dart index d3e0c4f5e..63bc7ec72 100644 --- a/forui/lib/src/widgets/header/header.dart +++ b/forui/lib/src/widgets/header/header.dart @@ -10,13 +10,13 @@ part 'header_action.dart'; /// A header. /// -/// A header contains the page's title and navigation actions. It is typically used on pages at the root of the -/// navigation stack. +/// A header contains the page's title and navigation actions. +/// It is typically used on pages at the root of the navigation stack. /// /// See: /// * https://forui.dev/docs/header for working examples. /// * [FHeaderStyle] for customizing a header's appearance. -class FHeader extends StatelessWidget { +final class FHeader extends StatelessWidget { /// The header's style. Defaults to [FThemeData.headerStyle]. final FHeaderStyle? style; @@ -79,7 +79,7 @@ class FHeader extends StatelessWidget { child: title, ), ), - Row(children: actions), + Row(children: actions.expand((action) => [action, const SizedBox(width: 10)]).toList()), ], ), ), @@ -102,7 +102,10 @@ final class FHeaderStyle with Diagnosticable { final TextStyle titleTextStyle; /// The [FHeaderAction]'s style. - final FHeaderActionStyle action; + final FHeaderActionStyle actionStyle; + + /// The spacing between [FHeaderAction]s. + final double actionSpacing; /// The padding. final EdgeInsets padding; @@ -110,7 +113,8 @@ final class FHeaderStyle with Diagnosticable { /// Creates a [FHeaderStyle]. FHeaderStyle({ required this.titleTextStyle, - required this.action, + required this.actionStyle, + required this.actionSpacing, required this.padding, }); @@ -124,7 +128,8 @@ final class FHeaderStyle with Diagnosticable { fontWeight: FontWeight.w700, height: 1, ), - action = FHeaderActionStyle.inherit(colorScheme: colorScheme), + actionStyle = FHeaderActionStyle.inherit(colorScheme: colorScheme), + actionSpacing = 10, padding = style.pagePadding.copyWith(bottom: 15); /// Returns a copy of this [FHeaderStyle] with the given properties replaced. @@ -145,12 +150,14 @@ final class FHeaderStyle with Diagnosticable { @useResult FHeaderStyle copyWith({ TextStyle? titleTextStyle, - FHeaderActionStyle? action, + FHeaderActionStyle? actionStyle, + double? actionSpacing, EdgeInsets? padding, }) => FHeaderStyle( titleTextStyle: titleTextStyle ?? this.titleTextStyle, - action: action ?? this.action, + actionStyle: actionStyle ?? this.actionStyle, + actionSpacing: actionSpacing ?? this.actionSpacing, padding: padding ?? this.padding, ); @@ -159,7 +166,8 @@ final class FHeaderStyle with Diagnosticable { super.debugFillProperties(properties); properties ..add(DiagnosticsProperty('titleTextStyle', titleTextStyle)) - ..add(DiagnosticsProperty('action', action)) + ..add(DiagnosticsProperty('action', actionStyle)) + ..add(DiagnosticsProperty('actionSpacing', actionSpacing)) ..add(DiagnosticsProperty('padding', padding)); } } diff --git a/forui/lib/src/widgets/header/header_action.dart b/forui/lib/src/widgets/header/header_action.dart index 9c16973af..64ebf5e59 100644 --- a/forui/lib/src/widgets/header/header_action.dart +++ b/forui/lib/src/widgets/header/header_action.dart @@ -31,20 +31,17 @@ class FHeaderAction extends StatelessWidget { @override Widget build(BuildContext context) { - final style = this.style ?? context.theme.headerStyle.action; + final style = this.style ?? context.theme.headerStyle.actionStyle; final enabled = onPress != null || onLongPress != null; - return Padding( - padding: style.padding, - child: MouseRegion( - cursor: enabled ? SystemMouseCursors.click : MouseCursor.defer, - child: FTappable( - onTap: onPress, - onLongPress: onLongPress, - child: icon( - height: style.size, - colorFilter: ColorFilter.mode(onPress == null ? style.disabledColor : style.enabledColor, BlendMode.srcIn), - ), + return MouseRegion( + cursor: enabled ? SystemMouseCursors.click : MouseCursor.defer, + child: FTappable( + onTap: onPress, + onLongPress: onLongPress, + child: icon( + height: style.size, + colorFilter: ColorFilter.mode(onPress == null ? style.disabledColor : style.enabledColor, BlendMode.srcIn), ), ), ); @@ -72,23 +69,18 @@ class FHeaderActionStyle with Diagnosticable { /// The icon's size. Defaults to 30. final double size; - /// The padding. Defaults to `EdgeInsets.only(left: 10)`. - final EdgeInsets padding; - /// Creates a [FHeaderActionStyle]. FHeaderActionStyle({ required this.enabledColor, required this.disabledColor, this.size = 30, - this.padding = const EdgeInsets.only(left: 10), }); /// Creates a [FHeaderActionStyle] that inherits its properties from the given [FColorScheme]. FHeaderActionStyle.inherit({required FColorScheme colorScheme}) : enabledColor = colorScheme.foreground, disabledColor = colorScheme.foreground.withOpacity(0.5), - size = 30, - padding = const EdgeInsets.only(left: 10); + size = 30; /// Returns a copy of this [FHeaderActionStyle] with the given properties replaced. /// @@ -116,7 +108,6 @@ class FHeaderActionStyle with Diagnosticable { enabledColor: enabledColor ?? this.enabledColor, disabledColor: disabledColor ?? this.disabledColor, size: size ?? this.size, - padding: padding ?? this.padding, ); @override @@ -125,7 +116,6 @@ class FHeaderActionStyle with Diagnosticable { properties ..add(ColorProperty('enabledColor', enabledColor)) ..add(ColorProperty('disabledColor', disabledColor)) - ..add(DoubleProperty('size', size, defaultValue: 30)) - ..add(DiagnosticsProperty('padding', padding, defaultValue: const EdgeInsets.only(left: 10))); + ..add(DoubleProperty('size', size, defaultValue: 30)); } } diff --git a/forui/lib/src/widgets/nested_header/nested_header.dart b/forui/lib/src/widgets/nested_header/nested_header.dart new file mode 100644 index 000000000..987054c5b --- /dev/null +++ b/forui/lib/src/widgets/nested_header/nested_header.dart @@ -0,0 +1,202 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; + +import 'package:forui/forui.dart'; +import 'package:forui/src/foundation/tappable.dart'; +import 'package:meta/meta.dart'; + +part 'nested_header_action.dart'; + +/// A nested header. +/// +/// A nested header contains the page's title and navigation actions. +/// It is typically used on pages that are not at the root of the navigation stack. +/// +/// See: +/// * https://forui.dev/docs/nested-header for working examples. +/// * [FNestedHeaderStyle] for customizing a header's appearance. +final class FNestedHeader extends StatelessWidget { + /// The style. Defaults to [FThemeData.nestedHeaderStyle]. + final FNestedHeaderStyle? style; + + /// The title, aligned to the center. + /// + /// ## Contract: + /// Throws [AssertionError]: + /// * if [title] and [rawTitle] are both not null + /// * if [title] and [rawTitle] are both null + final String? title; + + /// The title, aligned to the center. + /// + /// ## Contract: + /// Throws [AssertionError]: + /// * if [title] and [rawTitle] are both not null + /// * if [title] and [rawTitle] are both null + final Widget? rawTitle; + + /// The actions, aligned to the left. Defaults to an empty list. + final List leftActions; + + /// The actions, aligned to the right. Defaults to an empty list. + final List rightActions; + + /// Creates a [FNestedHeader] with an [onPop] callback. + /// + /// ## Contract: + /// Throws [AssertionError] if: + /// * [title] and [rawTitle] are both not null. + FNestedHeader({ + required VoidCallback? onPop, + this.style, + this.title, + this.rawTitle, + this.rightActions = const [], + super.key, + }) : assert((title != null) ^ (rawTitle != null), 'title or rawTitle must be provided, but not both.'), + leftActions = [ + if (onPop != null) + FNestedHeaderAction( + icon: FAssets.icons.arrowLeft, + onPress: onPop, + ), + ]; + + /// Creates a [FNestedHeader]. + /// + /// ## Contract: + /// Throws [AssertionError] if: + /// * [title] and [rawTitle] are both not null. + const FNestedHeader.raw({ + this.style, + this.title, + this.rawTitle, + this.leftActions = const [], + this.rightActions = const [], + super.key, + }) : assert((title != null) ^ (rawTitle != null), 'title or rawTitle must be provided, but not both.'); + + @override + Widget build(BuildContext context) { + final style = this.style ?? context.theme.nestedHeaderStyle; + + final title = switch ((this.title, rawTitle)) { + (final String title, _) => Text(title), + (_, final Widget title) => title, + _ => const Placeholder(), + }; + + return SafeArea( + bottom: false, + child: Padding( + padding: style.padding, + child: Stack( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row(children: leftActions.expand((action) => [action, const SizedBox(width: 10)]).toList()), + Row(children: rightActions.expand((action) => [const SizedBox(width: 10), action]).toList()), + ], + ), + Positioned.fill( + child: Align( + child: DefaultTextStyle.merge( + overflow: TextOverflow.fade, + maxLines: 1, + softWrap: false, + style: style.titleTextStyle, + child: title, + ), + ), + ), + ], + ), + ), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('style', style)) + ..add(StringProperty('title', title)) + ..add(DiagnosticsProperty('rawTitle', rawTitle)); + } +} + +/// [FNestedHeader]'s style. +final class FNestedHeaderStyle with Diagnosticable { + /// The title's [TextStyle]. + final TextStyle titleTextStyle; + + /// The [FHeaderAction]'s style for [FNestedHeader.leftActions] and [FNestedHeader.rightActions]. + final FNestedHeaderActionStyle actionStyle; + + /// The spacing between [FHeaderAction]s. + final double actionSpacing; + + /// The padding. + final EdgeInsets padding; + + /// Creates a [FNestedHeaderStyle]. + const FNestedHeaderStyle({ + required this.titleTextStyle, + required this.actionStyle, + required this.actionSpacing, + required this.padding, + }); + + /// Creates a [FNestedHeaderStyle] that inherits its properties from the given [FColorScheme] and [FTypography]. + FNestedHeaderStyle.inherit({ + required FColorScheme colorScheme, + required FTypography typography, + required FStyle style, + }) : titleTextStyle = typography.xl.copyWith( + color: colorScheme.foreground, + fontWeight: FontWeight.w600, + height: 1, + ), + actionStyle = FNestedHeaderActionStyle.inherit(colorScheme: colorScheme), + actionSpacing = 10, + padding = style.pagePadding.copyWith(bottom: 15); + + /// Returns a copy of this [FNestedHeaderStyle] with the given properties replaced. + /// + /// ```dart + /// final style = FNestedHeaderStyle( + /// titleTextStyle: ..., + /// leftAction: ..., + /// ); + /// + /// final copy = style.copyWith( + /// leftAction: ..., + /// ); + /// + /// print(style.titleTextStyle == copy.titleTextStyle); // true + /// print(style.leftAction == copy.leftAction); // false + /// ``` + FNestedHeaderStyle copyWith({ + TextStyle? titleTextStyle, + FNestedHeaderActionStyle? actionStyle, + double? actionSpacing, + EdgeInsets? padding, + }) => + FNestedHeaderStyle( + titleTextStyle: titleTextStyle ?? this.titleTextStyle, + actionStyle: actionStyle ?? this.actionStyle, + actionSpacing: actionSpacing ?? this.actionSpacing, + padding: padding ?? this.padding, + ); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('titleTextStyle', titleTextStyle)) + ..add(DiagnosticsProperty('actionStyle', actionStyle)) + ..add(DiagnosticsProperty('actionSpacing', actionSpacing)) + ..add(DiagnosticsProperty('padding', padding)); + } +} diff --git a/forui/lib/src/widgets/nested_header/nested_header_action.dart b/forui/lib/src/widgets/nested_header/nested_header_action.dart new file mode 100644 index 000000000..b385df0db --- /dev/null +++ b/forui/lib/src/widgets/nested_header/nested_header_action.dart @@ -0,0 +1,121 @@ +part of 'nested_header.dart'; + +/// A [FNestedHeader] action. +/// +/// If the [onPress] and [onLongPress] callbacks are null, then this action will be disabled, it will not react to touch. +class FNestedHeaderAction extends StatelessWidget { + /// The style. + final FNestedHeaderActionStyle? style; + + /// The icon. + final SvgAsset icon; + + /// A callback for when the button is pressed. + /// + /// The action will be disabled if both [onPress] and [onLongPress] are null. + final VoidCallback? onPress; + + /// A callback for when the button is long pressed. + /// + /// The action will be disabled if both [onPress] and [onLongPress] are null. + final VoidCallback? onLongPress; + + /// Creates a [FNestedHeaderAction] from the given SVG [icon]. + const FNestedHeaderAction({ + required this.icon, + required this.onPress, + this.onLongPress, + this.style, + super.key, + }); + + @override + Widget build(BuildContext context) { + final style = this.style ?? context.theme.nestedHeaderStyle.actionStyle; + final enabled = onPress != null || onLongPress != null; + + return MouseRegion( + cursor: enabled ? SystemMouseCursors.click : MouseCursor.defer, + child: FTappable( + onTap: onPress, + onLongPress: onLongPress, + child: icon( + height: style.size, + colorFilter: ColorFilter.mode(onPress == null ? style.disabledColor : style.enabledColor, BlendMode.srcIn), + ), + ), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('style', style)) + ..add(DiagnosticsProperty('icon', icon)) + ..add(DiagnosticsProperty('onPress', onPress)) + ..add(DiagnosticsProperty('onLongPress', onLongPress)); + } +} + +/// [FNestedHeaderAction]'s style. +class FNestedHeaderActionStyle with Diagnosticable { + /// The icon's color when this action is enabled. + final Color enabledColor; + + /// The icon's color when this action is disabled. + final Color disabledColor; + + /// The icon's size. Defaults to 20. + final double size; + + /// Creates a [FNestedHeaderActionStyle]. + FNestedHeaderActionStyle({ + required this.enabledColor, + required this.disabledColor, + this.size = 25, + }); + + /// Creates a [FNestedHeaderActionStyle] that inherits its properties from the given [FColorScheme]. + FNestedHeaderActionStyle.inherit({required FColorScheme colorScheme}) + : enabledColor = colorScheme.foreground, + disabledColor = colorScheme.foreground.withOpacity(0.5), + size = 25; + + /// Returns a copy of this [FNestedHeaderActionStyle] with the given properties replaced. + /// + /// ```dart + /// final style = FNestedHeaderActionStyle( + /// enabledColor: Colors.black, + /// disabledColor: Colors.white, + /// ); + /// + /// final copy = style.copyWith( + /// disabledColor: Colors.blue, + /// ); + /// + /// print(copy.enabledColor); // black + /// print(copy.disabledColor); // blue + /// ``` + @useResult + FNestedHeaderActionStyle copyWith({ + Color? enabledColor, + Color? disabledColor, + double? size, + EdgeInsets? padding, + }) => + FNestedHeaderActionStyle( + enabledColor: enabledColor ?? this.enabledColor, + disabledColor: disabledColor ?? this.disabledColor, + size: size ?? this.size, + ); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(ColorProperty('enabledColor', enabledColor)) + ..add(ColorProperty('disabledColor', disabledColor)) + ..add(DoubleProperty('size', size, defaultValue: 30)); + } +}