diff --git a/docs/pages/docs/_meta.json b/docs/pages/docs/_meta.json index bed9fbf34..823f943f4 100644 --- a/docs/pages/docs/_meta.json +++ b/docs/pages/docs/_meta.json @@ -1,7 +1,7 @@ { "index": "Getting Started", "themes": "Themes", - "icon": "Icon", + "icon-library": "Icon", "api_reference": { "title": "API Reference ↗", "href": "https://pub.dev/documentation/forui", diff --git a/docs/pages/docs/alert.mdx b/docs/pages/docs/alert.mdx index 1bfb17b2f..25e49497f 100644 --- a/docs/pages/docs/alert.mdx +++ b/docs/pages/docs/alert.mdx @@ -30,7 +30,7 @@ Displays a callout for user attention. ```dart FAlert( - icon: FAlertIcon(icon: FAssets.icons.badgeAlert), + icon: FIcon(FAssets.icons.badgeAlert), title: const Text('Heads Up!'), subtitle: const Text('You can add components to your app using the cli.'), ); diff --git a/docs/pages/docs/bottom-navigation-bar.mdx b/docs/pages/docs/bottom-navigation-bar.mdx index 571916f52..ffd74245a 100644 --- a/docs/pages/docs/bottom-navigation-bar.mdx +++ b/docs/pages/docs/bottom-navigation-bar.mdx @@ -34,23 +34,23 @@ It is used to navigate between a small number of views, typically between three onChange: (index) => setState(() => this.index = index), children: [ FBottomNavigationBarItem( - icon: FAssets.icons.home, + icon: FIcon(FAssets.icons.home), label: const Text('Home'), ), FBottomNavigationBarItem( - icon: FAssets.icons.layoutGrid, + icon: FIcon(FAssets.icons.layoutGrid), label: const Text('Browse'), ), FBottomNavigationBarItem( - icon: FAssets.icons.radio, + icon: FIcon(FAssets.icons.radio), label: const Text('Radio'), ), FBottomNavigationBarItem( - icon: FAssets.icons.libraryBig, + icon: FIcon(FAssets.icons.libraryBig), label: const Text('Library'), ), FBottomNavigationBarItem( - icon: FAssets.icons.search, + icon: FIcon(FAssets.icons.search), label: const Text('Search'), ), ], @@ -74,81 +74,9 @@ FBottomNavigationBar( onChange: (index) => {}, children: [ FBottomNavigationBarItem( - icon: FAssets.icons.home, + icon: FIcon(FAssets.icons.home), label: const Text('Home'), ), ], ) ``` - -## Examples - -### Custom Icon - - - - - - - ```dart - class Application extends StatefulWidget { - const Application({super.key}); - - @override - State createState() => _ApplicationState(); - } - - class _ApplicationState extends State { - int index = 1; - - @override - Widget build(BuildContext context) => FBottomNavigationBar( - index: index, - onChange: (index) => setState(() => this.index = index), - children: [ - FBottomNavigationBarItem.customIcon( - iconBuilder: (_, data, __) => Icon( - Icons.home_outlined, - size: data.itemStyle.iconSize, - color: data.selected ? data.itemStyle.activeIconColor : data.itemStyle.inactiveIconColor, - ), - label: const Text('Home'), - ), - FBottomNavigationBarItem.customIcon( - iconBuilder: (_, data, __) => Icon( - Icons.browse_gallery_outlined, - size: data.itemStyle.iconSize, - color: data.selected ? data.itemStyle.activeIconColor : data.itemStyle.inactiveIconColor, - ), - label: const Text('Browse'), - ), - FBottomNavigationBarItem.customIcon( - iconBuilder: (_, data, __) => Icon( - Icons.radio_outlined, - size: data.itemStyle.iconSize, - color: data.selected ? data.itemStyle.activeIconColor : data.itemStyle.inactiveIconColor, - ), - label: const Text('Radio'), - ), - FBottomNavigationBarItem.customIcon( - iconBuilder: (_, data, __) => Icon( - Icons.library_books_outlined, - size: data.itemStyle.iconSize, - color: data.selected ? data.itemStyle.activeIconColor : data.itemStyle.inactiveIconColor, - ), - label: const Text('Library'), - ), - FBottomNavigationBarItem.customIcon( - iconBuilder: (_, data, __) => Icon( - Icons.search_outlined, - size: data.itemStyle.iconSize, - color: data.selected ? data.itemStyle.activeIconColor : data.itemStyle.inactiveIconColor, - ), - label: const Text('Search'), - ), - ], - ); - } - ``` - - diff --git a/docs/pages/docs/button.mdx b/docs/pages/docs/button.mdx index 0ae156848..76366fa91 100644 --- a/docs/pages/docs/button.mdx +++ b/docs/pages/docs/button.mdx @@ -122,7 +122,7 @@ FButton.raw( ```dart {2} FButton( - prefix: FButtonIcon(icon: FAssets.icons.mail), + prefix: FIcon(icon: FAssets.icons.mail), label: const Text('Login with Email'), onPress: () {}, ), @@ -138,7 +138,7 @@ FButton.raw( ```dart {2} FButton.icon( - icon: FButtonIcon(icon: FAssets.icons.chevronRight), + icon: FIcon(icon: FAssets.icons.chevronRight), onPress: () {}, ), ``` diff --git a/docs/pages/docs/icon-library.mdx b/docs/pages/docs/icon-library.mdx new file mode 100644 index 000000000..64584e1c6 --- /dev/null +++ b/docs/pages/docs/icon-library.mdx @@ -0,0 +1,57 @@ +import {Callout} from "nextra/components"; + + +# Icon + +Forui Assets is flutter library that provides a set of high-quality icons from [Lucide](https://lucide.dev/). + + +## Installation + + + Forui Assets is bundled with forui package. You don't need to install it separately if you installed `forui`. + + +From your Flutter project directory, run the following command to install Forui Assets. + +```bash filename="bash" +flutter pub install forui_assets +``` + + +## Usage + + + The best way to find a list of icons is to visit the [Lucide](https://lucide.dev/icons/) website. + We periodically update the icons in the Forui Assets package. + If you notice a missing icon in Forui Assets, please [open an issue](https://github.com/forus-labs/forui/issues/new). + + + + While you can use an icon from `forui_assets` directly, it is recommended to wrap it in an [FIcon](/docs/icon) to + automatically configure its color and size. + + +```dart +import 'package:forui/forui.dart'; + +// alternatively; if you've only installed forui_assets. +import 'package:forui_assets/forui_assets.dart'; + +// Dog icon as a Widget. It is recommended to wrap icons in FIcon if you're using Forui. +final dogIconWidget = FIcon(FAssets.icons.dog); + +// Bird icon as a Widget. +final birdIconWidget = FAssets.icons.bird(); + +// White cat icon with a size of 24x24. +final catIconWidget = FAssets.icons.cat( + width: 24, + height: 24, + colorFilter: const ColorFilter.mode(Color(0xFFFFFFFF), BlendMode.srcIn), +); + +// Saving an icon to a variable for later use. +final rabbitSvgAsset = FAssets.icons.rabbit; +final rabbitIconWidget = rabbitSvgAsset(); +``` \ No newline at end of file diff --git a/docs/pages/docs/icon.mdx b/docs/pages/docs/icon.mdx index 6a5102688..1b691ae01 100644 --- a/docs/pages/docs/icon.mdx +++ b/docs/pages/docs/icon.mdx @@ -1,49 +1,221 @@ -import {Callout} from "nextra/components"; - +import { Tabs } from 'nextra/components'; +import { Callout } from "nextra/components"; +import { Widget } from "../../components/widget"; +import LinkBadge from "../../components/link-badge/link-badge"; +import LinkBadgeGroup from "../../components/link-badge/link-badge-group"; # Icon +An icon that inherits its style from an enclosing, supported widget, such as a [button](/docs/button). + + + + + + + + + + + ```dart + Row( + children: [ + FButton.icon( + style: FButtonStyle.primary, + child: FIcon(FAssets.icons.bird), + onPress: () {}, + ), + const SizedBox(width: 10), + FButton.icon( + style: FButtonStyle.secondary, + child: FIcon(FAssets.icons.bird), + onPress: () {}, + ), + ] + ); + ``` + + -Forui Icons is flutter library that provides a set of high-quality icons from [Lucide](https://lucide.dev/). - - -## Installation - - Forui Icons is bundled with forui package. You don't need to install it separately if you installed `forui`. - +## Usage -From your Flutter project directory, run the following command to install Forui Icon. +### `FIcon(...)` -```bash filename="bash" -flutter pub install forui_assets +```dart +FIcon( + FAssets.icons.bird, + color: Colors.red, + size: 24, + semanticLabel: 'Label', +); ``` - -## Usage +### `FIcon.data(...)` - The best way to find a list of icons is to visit the [Lucide](https://lucide.dev/icons/) website. - We periodically update the icons in the Forui Icons package. - If you notice a missing icon in Forui Icons, please [open an issue](https://github.com/forus-labs/forui/issues/new). + This should be used with icons in other libraries, such as Cupertino and Material, that embed their icons as + `IconData`s. ```dart -import 'package:forui/forui.dart'; +FIcon.data( + Icons.abc, + color: Colors.red, + size: 24, + semanticLabel: 'Label', +); +``` + +### `FIcon.image(...)` -// alternatively; if you've only installed forui_assets. -import 'package:forui_assets/forui_assets.dart'; +```dart +FIcon.image( + NetworkImage('https://raw.githubusercontent.com/forus-labs/forui/main/samples/assets/avatar.png'), + color: Colors.red, + size: 24, + semanticLabel: 'Label', +); +``` -// Bird icon as a Widget. -final birdIconWidget = FAssets.icons.bird(); +### `FIcon.raw(...)` -// White cat icon with a size of 24x24. -final catIconWidget = FAssets.icons.cat( - width: 24, - height: 24, - colorFilter: const ColorFilter.mode(Color(0xFFFFFFFF), BlendMode.srcIn), +```dart +FIcon.raw( + builder: (context, style, child) => Container( + color: style.color, + height: style.size, + width: style.size, + child: child!, + ), + child: const Text('Button'), ); +``` + +## Examples + +### Bundled/SVG + + + + + + + ```dart {3} + FButton.icon( + style: FButtonStyle.secondary, + child: FIcon(FAssets.icons.wifi), + onPress: () {}, + ); + ``` + + + +### `IconData` + + + + + + + + + ```dart {3} + FButton.icon( + style: FButtonStyle.secondary, + child: const FIcon.data(Icons.wifi), + onPress: () {}, + ); + ``` + + + +### Image + + + It is important that the image's background is transparent. An image with a non-transparent background will be + completely filled with a single color. + -// Saving an icon to a variable for later use. -final rabbitSvgAsset = FAssets.icons.rabbit; -final rabbitIconWidget = rabbitSvgAsset(); -``` \ No newline at end of file + + + + + + ```dart {7, 14} + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Original color + FButton.icon( + style: FButtonStyle.primary, + child: FIcon.image(AssetImage(path('forus-labs.png')), color: Colors.transparent), + onPress: () {}, + ), + const SizedBox(width: 10), + // Recolored + FButton.icon( + style: FButtonStyle.primary, + child: FIcon.image(AssetImage(path('forus-labs.png'))), + onPress: () {}, + ), + ], + ); + ``` + + + +### Raw + + + + + + + ```dart {24-35} + class _Example extends StatefulWidget { + @override + State<_Example> createState() => _ExampleState(); + } + + class _ExampleState extends State<_Example> with SingleTickerProviderStateMixin { + late AnimationController controller; + late Animation animation; + + @override + void initState() { + super.initState(); + controller = AnimationController( + vsync: this, + duration: const Duration(seconds: 3), + ) + ..forward() + ..repeat(reverse: true); + animation = Tween(begin: 0.0, end: 1.0).animate(controller); + } + + @override + Widget build(BuildContext context) => FButton.icon( + child: FIcon.raw(builder: (context, style, child) { + // You can access widget specific inherited data inside here. + final FButtonData(:enabled) = FButtonData.of(context); + return enabled ? + AnimatedIcon( + icon: AnimatedIcons.home_menu, + progress: animation, + color: style.color, + size: style.size, + semanticLabel: 'Home menu', + ) : const FIcon.data(Icons.menu); + }), + onPress: () {}, + ); + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + } + ``` + + \ No newline at end of file diff --git a/forui/CHANGELOG.md b/forui/CHANGELOG.md index e6c7c940b..42ec6f9ec 100644 --- a/forui/CHANGELOG.md +++ b/forui/CHANGELOG.md @@ -3,8 +3,6 @@ ### Additions * Add `FSlider` -* Add `FBottomNavigationBarItem.custom(...)`. - * Add `FButtonCustomStyle.enabledHoverBoxDecoration`. * Add `FTextField.contentInsertionConfiguration`. @@ -16,6 +14,8 @@ * **Breaking** Add `FColorScheme.disabledColorBrightness` - this will only affect users that create a `FColorScheme` from scratch. +* Add `FIcon`. + ### Changes * Change button to change color when hovering over it. @@ -37,6 +37,17 @@ * **Breaking** Rename `FTextField.onSave` to `FTextField.onSaved`. +* **Breaking** Remove FAlertIcon & FAlertIconStyle - use `FIcon` instead. + +* **Breaking** Remove FButtonIcon & FAlertIconStyle - use `FIcon` instead. + +* * **Breaking** Change FButtonCustomStyle to better represent the style's layout - this will only affect users that + create a custom `FButtonCustomStyle`. + +* **Breaking** Change `FBottomNavigationBarItem.icon` from `SvgAsset` to `Widget` - wrap the asset in ` FIcon` instead. + +* **Breaking** Change `FHeaderAction.icon` from `SvgAsset` to `Widget` - wrap the asset in ` FIcon` instead. + ### Fixes * Fix `FBottomNavigationBar` items hit region being smaller than intended. diff --git a/forui/example/lib/main.dart b/forui/example/lib/main.dart index 914bf0283..2baf702d2 100644 --- a/forui/example/lib/main.dart +++ b/forui/example/lib/main.dart @@ -41,7 +41,7 @@ class _ApplicationState extends State { title: const Text('Example'), actions: [ FHeaderAction( - icon: FAssets.icons.plus, + icon: FIcon(FAssets.icons.plus), onPress: () {}, ), ], @@ -52,23 +52,23 @@ class _ApplicationState extends State { onChange: (index) => setState(() => this.index = index), children: [ FBottomNavigationBarItem( - icon: FAssets.icons.home, + icon: FIcon(FAssets.icons.home), label: const Text('Home'), ), FBottomNavigationBarItem( - icon: FAssets.icons.layoutGrid, + icon: FIcon(FAssets.icons.layoutGrid), label: const Text('Categories'), ), FBottomNavigationBarItem( - icon: FAssets.icons.search, + icon: FIcon(FAssets.icons.search), label: const Text('Search'), ), FBottomNavigationBarItem( - icon: FAssets.icons.settings, + icon: FIcon(FAssets.icons.settings), label: const Text('Settings'), ), FBottomNavigationBarItem( - icon: FAssets.icons.castle, + icon: FIcon(FAssets.icons.castle), label: const Text('Sandbox'), ), ], diff --git a/forui/example/pubspec.yaml b/forui/example/pubspec.yaml index b29afe800..da90e7713 100644 --- a/forui/example/pubspec.yaml +++ b/forui/example/pubspec.yaml @@ -47,7 +47,7 @@ flutter: # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in # the material Icons class. - uses-material-design: false + uses-material-design: true # To add assets to your application, add an assets section, like this: # assets: diff --git a/forui/lib/forui.dart b/forui/lib/forui.dart index 248cc0135..4ceb8001f 100644 --- a/forui/lib/forui.dart +++ b/forui/lib/forui.dart @@ -17,6 +17,7 @@ export 'widgets/checkbox.dart'; export 'widgets/dialog.dart'; export 'widgets/divider.dart'; export 'widgets/header.dart'; +export 'widgets/icon.dart'; export 'widgets/label.dart'; export 'widgets/popover.dart'; export 'widgets/progress.dart'; diff --git a/forui/lib/foundation.dart b/forui/lib/foundation.dart index a614fa2aa..bf706ca5f 100644 --- a/forui/lib/foundation.dart +++ b/forui/lib/foundation.dart @@ -1,6 +1,6 @@ /// Low-level utilities and services. library forui.foundation; -export 'src/foundation/layout.dart'; +export 'src/foundation/rendering.dart' hide Alignments, RenderBoxes; export 'src/foundation/portal/portal.dart'; export 'src/foundation/portal/portal_shift.dart'; diff --git a/forui/lib/src/foundation/layout.dart b/forui/lib/src/foundation/layout.dart deleted file mode 100644 index 95a12e05f..000000000 --- a/forui/lib/src/foundation/layout.dart +++ /dev/null @@ -1,19 +0,0 @@ -/// Possible way to layout a sequence of items. -enum Layout { - /// Lays out the items horizontally from left to right. - ltr(vertical: false), - - /// Lays out the items horizontally from right to left. - rtl(vertical: false), - - /// Lays out the items vertically from bottom to top. - ttb(vertical: true), - - /// Lays out the items vertically from top to bottom. - btt(vertical: true); - - /// Whether the layout is vertical. - final bool vertical; - - const Layout({required this.vertical}); -} diff --git a/forui/lib/src/foundation/rendering.dart b/forui/lib/src/foundation/rendering.dart index 9c987942f..4000c397a 100644 --- a/forui/lib/src/foundation/rendering.dart +++ b/forui/lib/src/foundation/rendering.dart @@ -2,6 +2,26 @@ import 'package:flutter/rendering.dart'; import 'package:meta/meta.dart'; +/// Possible way to layout a sequence of items. +enum Layout { + /// Lays out the items horizontally from left to right. + ltr(vertical: false), + + /// Lays out the items horizontally from right to left. + rtl(vertical: false), + + /// Lays out the items vertically from bottom to top. + ttb(vertical: true), + + /// Lays out the items vertically from top to bottom. + btt(vertical: true); + + /// Whether the layout is vertical. + final bool vertical; + + const Layout({required this.vertical}); +} + @internal extension RenderBoxes on RenderBox { BoxParentData get data => parentData! as BoxParentData; diff --git a/forui/lib/src/theme/style.dart b/forui/lib/src/theme/style.dart index fecd58f8b..834b3999b 100644 --- a/forui/lib/src/theme/style.dart +++ b/forui/lib/src/theme/style.dart @@ -4,7 +4,6 @@ import 'package:flutter/widgets.dart'; import 'package:meta/meta.dart'; import 'package:forui/forui.dart'; -import 'package:forui/theme.dart'; /// A set of miscellaneous properties that is part of a [FThemeData]. /// @@ -20,6 +19,9 @@ final class FStyle with Diagnosticable { /// The style for the form field when it has an error. final FFormFieldErrorStyle errorFormFieldStyle; + /// The icon style. + final FIconStyle iconStyle; + /// The border radius. Defaults to `BorderRadius.circular(8)`. final BorderRadius borderRadius; @@ -37,6 +39,7 @@ final class FStyle with Diagnosticable { required this.enabledFormFieldStyle, required this.disabledFormFieldStyle, required this.errorFormFieldStyle, + required this.iconStyle, this.borderRadius = const BorderRadius.all(Radius.circular(8)), this.borderWidth = 1, this.pagePadding = const EdgeInsets.symmetric(vertical: 8, horizontal: 12), @@ -63,6 +66,10 @@ final class FStyle with Diagnosticable { errorColor: colorScheme.error, typography: typography, ), + iconStyle: FIconStyle( + color: colorScheme.primary, + size: 20, + ), ); /// Returns a copy of this [FStyle] with the given properties replaced. @@ -71,6 +78,7 @@ final class FStyle with Diagnosticable { FFormFieldStyle? enabledFormFieldStyle, FFormFieldStyle? disabledFormFieldStyle, FFormFieldErrorStyle? errorFormFieldStyle, + FIconStyle? iconStyle, BorderRadius? borderRadius, double? borderWidth, EdgeInsets? pagePadding, @@ -79,6 +87,7 @@ final class FStyle with Diagnosticable { enabledFormFieldStyle: enabledFormFieldStyle ?? this.enabledFormFieldStyle, disabledFormFieldStyle: disabledFormFieldStyle ?? this.disabledFormFieldStyle, errorFormFieldStyle: errorFormFieldStyle ?? this.errorFormFieldStyle, + iconStyle: iconStyle ?? this.iconStyle, borderRadius: borderRadius ?? this.borderRadius, borderWidth: borderWidth ?? this.borderWidth, pagePadding: pagePadding ?? this.pagePadding, @@ -91,6 +100,7 @@ final class FStyle with Diagnosticable { ..add(DiagnosticsProperty('enabledFormFieldStyle', enabledFormFieldStyle)) ..add(DiagnosticsProperty('disabledFormFieldStyle', disabledFormFieldStyle)) ..add(DiagnosticsProperty('errorFormFieldStyle', errorFormFieldStyle)) + ..add(DiagnosticsProperty('iconStyle', iconStyle)) ..add(DiagnosticsProperty('borderRadius', borderRadius, defaultValue: BorderRadius.circular(8))) ..add(DoubleProperty('borderWidth', borderWidth, defaultValue: 1)) ..add(DiagnosticsProperty('pagePadding', pagePadding, defaultValue: const EdgeInsets.all(4))); @@ -104,6 +114,7 @@ final class FStyle with Diagnosticable { enabledFormFieldStyle == other.enabledFormFieldStyle && disabledFormFieldStyle == other.disabledFormFieldStyle && errorFormFieldStyle == other.errorFormFieldStyle && + iconStyle == other.iconStyle && borderRadius == other.borderRadius && borderWidth == other.borderWidth && pagePadding == other.pagePadding; @@ -113,6 +124,7 @@ final class FStyle with Diagnosticable { enabledFormFieldStyle.hashCode ^ disabledFormFieldStyle.hashCode ^ errorFormFieldStyle.hashCode ^ + iconStyle.hashCode ^ borderRadius.hashCode ^ borderWidth.hashCode ^ pagePadding.hashCode; diff --git a/forui/lib/src/widgets/alert/alert.dart b/forui/lib/src/widgets/alert.dart similarity index 59% rename from forui/lib/src/widgets/alert/alert.dart rename to forui/lib/src/widgets/alert.dart index a3e11e15d..68e1d765b 100644 --- a/forui/lib/src/widgets/alert/alert.dart +++ b/forui/lib/src/widgets/alert.dart @@ -14,6 +14,8 @@ import 'package:forui/forui.dart'; /// * [FAlertStyle] for customizing an alert's appearance. class FAlert extends StatelessWidget { /// The icon. Defaults to [FAssets.icons.circleAlert]. + /// + /// [icon] is wrapped in [FIconStyle], and therefore works with [FIcon]s. final Widget icon; /// The title. @@ -42,7 +44,7 @@ class FAlert extends StatelessWidget { this.subtitle, this.style = FAlertStyle.primary, super.key, - }) : icon = icon ?? FAlertIcon(icon: FAssets.icons.circleAlert); + }) : icon = icon ?? FIcon(FAssets.icons.circleAlert); @override Widget build(BuildContext context) { @@ -61,7 +63,10 @@ class FAlert extends StatelessWidget { children: [ Row( children: [ - InheritedData(style: style, child: icon), + FInheritedIconStyle( + style: FIconStyle(color: style.iconColor, size: style.iconSize), + child: icon, + ), Flexible( child: Padding( padding: const EdgeInsets.only(left: 8), @@ -76,7 +81,7 @@ class FAlert extends StatelessWidget { if (subtitle != null) Row( children: [ - SizedBox(width: style.icon.size), + SizedBox(width: style.iconSize), Flexible( child: Padding( padding: const EdgeInsets.only(top: 3, left: 8), @@ -101,6 +106,84 @@ class FAlert extends StatelessWidget { } } +/// [FAlertCustomStyle]'s style. +final class FAlertStyles with Diagnosticable { + /// The primary alert style. + final FAlertCustomStyle primary; + + /// The destructive alert style. + final FAlertCustomStyle destructive; + + /// Creates a [FAlertStyles]. + const FAlertStyles({ + required this.primary, + required this.destructive, + }); + + /// Creates a [FAlertStyles] that inherits its properties from the provided [colorScheme], [typography], and [style]. + FAlertStyles.inherit({required FColorScheme colorScheme, required FTypography typography, required FStyle style}) + : primary = FAlertCustomStyle( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 12), + iconColor: colorScheme.foreground, + titleTextStyle: typography.base.copyWith( + fontWeight: FontWeight.w500, + color: colorScheme.foreground, + height: 1.2, + ), + subtitleTextStyle: typography.sm.copyWith(color: colorScheme.foreground), + decoration: BoxDecoration( + border: Border.all(color: colorScheme.border), + borderRadius: style.borderRadius, + color: colorScheme.background, + ), + ), + destructive = FAlertCustomStyle( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 12), + iconColor: colorScheme.destructive, + titleTextStyle: typography.base.copyWith( + fontWeight: FontWeight.w500, + color: colorScheme.destructive, + height: 1.2, + ), + subtitleTextStyle: typography.sm.copyWith(color: colorScheme.destructive), + decoration: BoxDecoration( + border: Border.all(color: colorScheme.destructive), + borderRadius: style.borderRadius, + color: colorScheme.background, + ), + ); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('primary', primary)) + ..add(DiagnosticsProperty('destructive', destructive)); + } + + /// Returns a copy of this [FAlertStyles] with the given properties replaced. + @useResult + FAlertStyles copyWith({ + FAlertCustomStyle? primary, + FAlertCustomStyle? destructive, + }) => + FAlertStyles( + primary: primary ?? this.primary, + destructive: destructive ?? this.destructive, + ); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is FAlertStyles && + runtimeType == other.runtimeType && + primary == other.primary && + destructive == other.destructive; + + @override + int get hashCode => primary.hashCode ^ destructive.hashCode; +} + /// A [FAlert]'s style. /// /// A style can be either one of the pre-defined styles in [FAlertStyle] or a [FAlertCustomStyle]. The pre-defined @@ -131,8 +214,16 @@ final class FAlertCustomStyle extends FAlertStyle with Diagnosticable { /// The padding. Defaults to `const EdgeInsets.all(16)`. final EdgeInsets padding; - /// The icon's style. - final FAlertIconStyle icon; + /// The icon's color. + /// + /// Defaults to 20. + final Color iconColor; + + /// The icon's size. Defaults to 20. + /// + /// ## Contract + /// Throws [AssertionError] if `iconSize` is not positive. + final double iconSize; /// The title's [TextStyle]. final TextStyle titleTextStyle; @@ -143,25 +234,28 @@ final class FAlertCustomStyle extends FAlertStyle with Diagnosticable { /// Creates a [FAlertCustomStyle]. FAlertCustomStyle({ required this.decoration, - required this.icon, + required this.iconColor, required this.titleTextStyle, required this.subtitleTextStyle, this.padding = const EdgeInsets.all(16), - }); + this.iconSize = 20, + }) : assert(0 < iconSize, 'iconSize is $iconSize, but it should be positive.'); /// Returns a copy of this [FAlertCustomStyle] with the given properties replaced. @useResult FAlertCustomStyle copyWith({ BoxDecoration? decoration, EdgeInsets? padding, - FAlertIconStyle? icon, + Color? iconColor, + double? iconSize, TextStyle? titleTextStyle, TextStyle? subtitleTextStyle, }) => FAlertCustomStyle( decoration: decoration ?? this.decoration, padding: padding ?? this.padding, - icon: icon ?? this.icon, + iconColor: iconColor ?? this.iconColor, + iconSize: iconSize ?? this.iconSize, titleTextStyle: titleTextStyle ?? this.titleTextStyle, subtitleTextStyle: subtitleTextStyle ?? this.subtitleTextStyle, ); @@ -172,7 +266,8 @@ final class FAlertCustomStyle extends FAlertStyle with Diagnosticable { properties ..add(DiagnosticsProperty('decoration', decoration)) ..add(DiagnosticsProperty('padding', padding)) - ..add(DiagnosticsProperty('icon', icon)) + ..add(DiagnosticsProperty('iconColor', iconColor)) + ..add(DiagnosticsProperty('iconSize', iconSize)) ..add(DiagnosticsProperty('titleTextStyle', titleTextStyle)) ..add(DiagnosticsProperty('subtitleTextStyle', subtitleTextStyle)); } @@ -184,37 +279,16 @@ final class FAlertCustomStyle extends FAlertStyle with Diagnosticable { runtimeType == other.runtimeType && decoration == other.decoration && padding == other.padding && - icon == other.icon && + iconColor == other.iconColor && + iconSize == other.iconSize && titleTextStyle == other.titleTextStyle && subtitleTextStyle == other.subtitleTextStyle; @override int get hashCode => - decoration.hashCode ^ padding.hashCode ^ icon.hashCode ^ titleTextStyle.hashCode ^ subtitleTextStyle.hashCode; -} - -@internal -class InheritedData extends InheritedWidget { - @useResult - static FAlertCustomStyle of(BuildContext context) { - final theme = context.dependOnInheritedWidgetOfExactType(); - return theme?.style ?? context.theme.alertStyles.primary; - } - - final FAlertCustomStyle style; - - const InheritedData({ - required this.style, - required super.child, - super.key, - }); - - @override - bool updateShouldNotify(covariant InheritedData old) => style != old.style; - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add(DiagnosticsProperty('style', style)); - } + decoration.hashCode ^ + padding.hashCode ^ + iconColor.hashCode ^ + iconSize.hashCode & titleTextStyle.hashCode ^ + subtitleTextStyle.hashCode; } diff --git a/forui/lib/src/widgets/alert/alert_icon.dart b/forui/lib/src/widgets/alert/alert_icon.dart deleted file mode 100644 index 94766d4cd..000000000 --- a/forui/lib/src/widgets/alert/alert_icon.dart +++ /dev/null @@ -1,82 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/widgets.dart'; - -import 'package:meta/meta.dart'; - -import 'package:forui/forui.dart'; -import 'package:forui/src/widgets/alert/alert.dart'; - -/// A [FAlert]'s icon. -class FAlertIcon extends StatelessWidget { - /// The icon. - final SvgAsset icon; - - /// Creates a [FAlertIcon] from the given SVG [icon]. - const FAlertIcon({required this.icon, super.key}); - - @override - Widget build(BuildContext context) { - final FAlertCustomStyle(:icon) = InheritedData.of(context); - - return this.icon( - height: icon.size, - colorFilter: ColorFilter.mode(icon.color, BlendMode.srcIn), - ); - } - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add(DiagnosticsProperty('icon', icon)); - } -} - -/// [FAlertIcon]'s style. -final class FAlertIconStyle with Diagnosticable { - /// The icon's color. - final Color color; - - /// The icon's size. Defaults to 20. - final double size; - - /// Creates a [FButtonIconStyle]. - /// - /// ## Contract - /// Throws [AssertionError] if: - /// * `height` <= 0.0 - /// * `height` is Nan - FAlertIconStyle({ - required this.color, - this.size = 20, - }) : assert(0 < size, 'The dimension is $size, but it should be positive.'); - - /// Returns a copy of this [FAlertIconStyle] with the given properties replaced. - @useResult - FAlertIconStyle copyWith({ - Color? color, - double? size, - }) => - FAlertIconStyle( - color: color ?? this.color, - size: size ?? this.size, - ); - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - ..add(ColorProperty('color', color)) - ..add(DoubleProperty('size', size, defaultValue: 20)); - } - - @override - bool operator ==(Object other) => - identical(this, other) || - other is FButtonIconStyle && - runtimeType == other.runtimeType && - color == other.enabledColor && - size == other.size; - - @override - int get hashCode => color.hashCode ^ size.hashCode; -} diff --git a/forui/lib/src/widgets/alert/alert_styles.dart b/forui/lib/src/widgets/alert/alert_styles.dart deleted file mode 100644 index bad695a77..000000000 --- a/forui/lib/src/widgets/alert/alert_styles.dart +++ /dev/null @@ -1,84 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/widgets.dart'; - -import 'package:meta/meta.dart'; - -import 'package:forui/forui.dart'; - -/// [FAlertCustomStyle]'s style. -final class FAlertStyles with Diagnosticable { - /// The primary alert style. - final FAlertCustomStyle primary; - - /// The destructive alert style. - final FAlertCustomStyle destructive; - - /// Creates a [FAlertStyles]. - const FAlertStyles({ - required this.primary, - required this.destructive, - }); - - /// Creates a [FAlertStyles] that inherits its properties from the provided [colorScheme], [typography], and [style]. - FAlertStyles.inherit({required FColorScheme colorScheme, required FTypography typography, required FStyle style}) - : primary = FAlertCustomStyle( - padding: const EdgeInsets.fromLTRB(16, 12, 16, 12), - titleTextStyle: typography.base.copyWith( - fontWeight: FontWeight.w500, - color: colorScheme.foreground, - height: 1.2, - ), - subtitleTextStyle: typography.sm.copyWith(color: colorScheme.foreground), - decoration: BoxDecoration( - border: Border.all(color: colorScheme.border), - borderRadius: style.borderRadius, - color: colorScheme.background, - ), - icon: FAlertIconStyle(color: colorScheme.foreground), - ), - destructive = FAlertCustomStyle( - padding: const EdgeInsets.fromLTRB(16, 12, 16, 12), - titleTextStyle: typography.base.copyWith( - fontWeight: FontWeight.w500, - color: colorScheme.destructive, - height: 1.2, - ), - subtitleTextStyle: typography.sm.copyWith(color: colorScheme.destructive), - decoration: BoxDecoration( - border: Border.all(color: colorScheme.destructive), - borderRadius: style.borderRadius, - color: colorScheme.background, - ), - icon: FAlertIconStyle(color: colorScheme.destructive), - ); - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - ..add(DiagnosticsProperty('primary', primary)) - ..add(DiagnosticsProperty('destructive', destructive)); - } - - /// Returns a copy of this [FAlertStyles] with the given properties replaced. - @useResult - FAlertStyles copyWith({ - FAlertCustomStyle? primary, - FAlertCustomStyle? destructive, - }) => - FAlertStyles( - primary: primary ?? this.primary, - destructive: destructive ?? this.destructive, - ); - - @override - bool operator ==(Object other) => - identical(this, other) || - other is FAlertStyles && - runtimeType == other.runtimeType && - primary == other.primary && - destructive == other.destructive; - - @override - int get hashCode => primary.hashCode ^ destructive.hashCode; -} diff --git a/forui/lib/src/widgets/bottom_navigation_bar/bottom_navigation_bar_item.dart b/forui/lib/src/widgets/bottom_navigation_bar/bottom_navigation_bar_item.dart index f8cf887c7..6830d4de7 100644 --- a/forui/lib/src/widgets/bottom_navigation_bar/bottom_navigation_bar_item.dart +++ b/forui/lib/src/widgets/bottom_navigation_bar/bottom_navigation_bar_item.dart @@ -7,35 +7,21 @@ import 'package:forui/forui.dart'; /// A [FBottomNavigationBar] item. class FBottomNavigationBarItem extends StatelessWidget { - static ValueWidgetBuilder _icon(SvgAsset icon) => (_, data, __) => icon( - height: data.itemStyle.iconSize, - colorFilter: ColorFilter.mode( - data.selected ? data.itemStyle.activeIconColor : data.itemStyle.inactiveIconColor, - BlendMode.srcIn, - ), - ); - /// The style. final FBottomNavigationBarItemStyle? style; - /// The icon's builder. - final ValueWidgetBuilder iconBuilder; + /// The icon. + /// + /// [icon] is wrapped in [FIconStyle], and therefore works with [FIcon]s. + final Widget icon; /// The label. final Widget label; /// Creates a [FBottomNavigationBarItem]. - FBottomNavigationBarItem({ + const FBottomNavigationBarItem({ required this.label, - required SvgAsset icon, - this.style, - super.key, - }) : iconBuilder = _icon(icon); - - /// Creates a [FBottomNavigationBarItem] with a custom icon. - const FBottomNavigationBarItem.custom({ - required this.label, - required this.iconBuilder, + required this.icon, this.style, super.key, }); @@ -51,7 +37,15 @@ class FBottomNavigationBarItem extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - ExcludeSemantics(child: iconBuilder(context, data, null)), + ExcludeSemantics( + child: FInheritedIconStyle( + style: FIconStyle( + color: data.selected ? data.itemStyle.activeIconColor : data.itemStyle.inactiveIconColor, + size: data.itemStyle.iconSize, + ), + child: icon, + ), + ), const SizedBox(height: 2), DefaultTextStyle.merge( style: selected ? style.activeTextStyle : style.inactiveTextStyle, @@ -66,9 +60,7 @@ class FBottomNavigationBarItem extends StatelessWidget { @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); - properties - ..add(DiagnosticsProperty('style', style)) - ..add(ObjectFlagProperty.has('iconBuilder', iconBuilder)); + properties.add(DiagnosticsProperty('style', style)); } } diff --git a/forui/lib/src/widgets/button/button.dart b/forui/lib/src/widgets/button/button.dart index 7655bd49b..ed3ae3a43 100644 --- a/forui/lib/src/widgets/button/button.dart +++ b/forui/lib/src/widgets/button/button.dart @@ -62,14 +62,14 @@ class FButton extends StatelessWidget { /// Creates a [FButton] that contains a [prefix], [label], and [suffix]. /// + /// [prefix] and [suffix] are wrapped in [FIconStyle], and therefore works with [FIcon]s. + /// /// The button layout is as follows, assuming the locale is read from left to right: /// ``` /// |---------------------------------------| /// | [prefixIcon] [label] [suffixIcon] | /// |---------------------------------------| /// ``` - /// - /// [FButtonIcon] provides a convenient way to transform a bundled SVG icon into a [prefix] and [suffix]. FButton({ required this.onPress, required Widget label, @@ -81,13 +81,11 @@ class FButton extends StatelessWidget { Widget? prefix, Widget? suffix, super.key, - }) : child = Content( - prefix: prefix, - suffix: suffix, - label: label, - ); + }) : child = Content(prefix: prefix, suffix: suffix, label: label); /// Creates a [FButton] that contains only an icon. + /// + /// [child] is wrapped in [FIconStyle], and therefore works with [FIcon]s. FButton.icon({ required this.onPress, required Widget child, @@ -206,9 +204,6 @@ class FButtonCustomStyle extends FButtonStyle with Diagnosticable { /// The content's style. final FButtonContentStyle content; - /// The icon's style. - final FButtonIconStyle icon; - /// The icon content's style. final FButtonIconContentStyle iconContent; @@ -218,8 +213,7 @@ class FButtonCustomStyle extends FButtonStyle with Diagnosticable { required this.enabledHoverBoxDecoration, required this.disabledBoxDecoration, required this.content, - required this.icon, - this.iconContent = const FButtonIconContentStyle(), + required this.iconContent, }); /// Returns a copy of this [FButtonCustomStyle] with the given properties replaced. @@ -229,7 +223,6 @@ class FButtonCustomStyle extends FButtonStyle with Diagnosticable { BoxDecoration? enabledHoverBoxDecoration, BoxDecoration? disabledBoxDecoration, FButtonContentStyle? content, - FButtonIconStyle? icon, FButtonIconContentStyle? iconContent, }) => FButtonCustomStyle( @@ -237,7 +230,6 @@ class FButtonCustomStyle extends FButtonStyle with Diagnosticable { enabledHoverBoxDecoration: enabledHoverBoxDecoration ?? this.enabledHoverBoxDecoration, disabledBoxDecoration: disabledBoxDecoration ?? this.disabledBoxDecoration, content: content ?? this.content, - icon: icon ?? this.icon, iconContent: iconContent ?? this.iconContent, ); @@ -249,7 +241,6 @@ class FButtonCustomStyle extends FButtonStyle with Diagnosticable { ..add(DiagnosticsProperty('enabledHoverBoxDecoration', enabledHoverBoxDecoration)) ..add(DiagnosticsProperty('disabledBoxDecoration', disabledBoxDecoration)) ..add(DiagnosticsProperty('content', content)) - ..add(DiagnosticsProperty('icon', icon)) ..add(DiagnosticsProperty('iconContent', iconContent)); } @@ -262,7 +253,6 @@ class FButtonCustomStyle extends FButtonStyle with Diagnosticable { enabledHoverBoxDecoration == other.enabledHoverBoxDecoration && disabledBoxDecoration == other.disabledBoxDecoration && content == other.content && - icon == other.icon && iconContent == other.iconContent; @override @@ -271,7 +261,6 @@ class FButtonCustomStyle extends FButtonStyle with Diagnosticable { enabledHoverBoxDecoration.hashCode ^ disabledBoxDecoration.hashCode ^ content.hashCode ^ - icon.hashCode ^ iconContent.hashCode; } diff --git a/forui/lib/src/widgets/button/button_content.dart b/forui/lib/src/widgets/button/button_content.dart index 14b2f0daa..086735f0c 100644 --- a/forui/lib/src/widgets/button/button_content.dart +++ b/forui/lib/src/widgets/button/button_content.dart @@ -27,17 +27,23 @@ class Content extends StatelessWidget { padding: content.padding, child: DefaultTextStyle.merge( style: enabled ? content.enabledTextStyle : content.disabledTextStyle, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: separate( - [ - if (prefix != null) prefix!, - label, - if (suffix != null) suffix!, - ], - by: [ - const SizedBox(width: 10), - ], + child: FInheritedIconStyle( + style: FIconStyle( + color: enabled ? content.enabledIconColor : content.disabledIconColor, + size: content.iconSize, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: separate( + [ + if (prefix != null) prefix!, + label, + if (suffix != null) suffix!, + ], + by: [ + const SizedBox(width: 10), + ], + ), ), ), ), @@ -53,11 +59,17 @@ class IconContent extends StatelessWidget { @override Widget build(BuildContext context) { - final FButtonData(:style, enabled: _) = FButtonData.of(context); + final FButtonData(:style, :enabled) = FButtonData.of(context); return Padding( padding: style.iconContent.padding, - child: child, + child: FInheritedIconStyle( + style: FIconStyle( + color: enabled ? style.iconContent.enabled : style.iconContent.disabled, + size: style.iconContent.size, + ), + child: child, + ), ); } } @@ -73,11 +85,23 @@ final class FButtonContentStyle with Diagnosticable { /// The padding. final EdgeInsets padding; + /// The icon's color when this button is enabled. + final Color enabledIconColor; + + /// The icon's color when this button is disabled. + final Color disabledIconColor; + + /// The icon's size. Defaults to 20. + final double iconSize; + /// Creates a [FButtonContentStyle]. FButtonContentStyle({ required this.enabledTextStyle, required this.disabledTextStyle, required this.padding, + required this.enabledIconColor, + required this.disabledIconColor, + this.iconSize = 20, }); /// Creates a [FButtonContentStyle] that inherits its properties from the given [enabled] and [disabled]. @@ -85,11 +109,7 @@ final class FButtonContentStyle with Diagnosticable { required FTypography typography, required Color enabled, required Color disabled, - }) : padding = const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12.5, - ), - enabledTextStyle = typography.base.copyWith( + }) : enabledTextStyle = typography.base.copyWith( color: enabled, fontWeight: FontWeight.w500, height: 1, @@ -98,7 +118,14 @@ final class FButtonContentStyle with Diagnosticable { color: disabled, fontWeight: FontWeight.w500, height: 1, - ); + ), + padding = const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12.5, + ), + enabledIconColor = enabled, + disabledIconColor = disabled, + iconSize = 20; /// Returns a copy of this [FButtonContentStyle] with the given properties replaced. @useResult @@ -106,11 +133,17 @@ final class FButtonContentStyle with Diagnosticable { TextStyle? enabledTextStyle, TextStyle? disabledTextStyle, EdgeInsets? padding, + Color? enabledIconColor, + Color? disabledIconColor, + double? iconSize, }) => FButtonContentStyle( enabledTextStyle: enabledTextStyle ?? this.enabledTextStyle, disabledTextStyle: disabledTextStyle ?? this.disabledTextStyle, padding: padding ?? this.padding, + enabledIconColor: enabledIconColor ?? this.enabledIconColor, + disabledIconColor: disabledIconColor ?? this.disabledIconColor, + iconSize: iconSize ?? this.iconSize, ); @override @@ -119,7 +152,10 @@ final class FButtonContentStyle with Diagnosticable { properties ..add(DiagnosticsProperty('enabledTextStyle', enabledTextStyle)) ..add(DiagnosticsProperty('disabledTextStyle', disabledTextStyle)) - ..add(DiagnosticsProperty('padding', padding)); + ..add(DiagnosticsProperty('padding', padding)) + ..add(ColorProperty('enabledColor', enabledIconColor)) + ..add(ColorProperty('disabledColor', disabledIconColor)) + ..add(DoubleProperty('size', iconSize, defaultValue: 20)); } @override @@ -129,10 +165,19 @@ final class FButtonContentStyle with Diagnosticable { runtimeType == other.runtimeType && enabledTextStyle == other.enabledTextStyle && disabledTextStyle == other.disabledTextStyle && - padding == other.padding; + padding == other.padding && + enabledIconColor == other.enabledIconColor && + disabledIconColor == other.disabledIconColor && + iconSize == other.iconSize; @override - int get hashCode => enabledTextStyle.hashCode ^ disabledTextStyle.hashCode ^ padding.hashCode; + int get hashCode => + enabledTextStyle.hashCode ^ + disabledTextStyle.hashCode ^ + padding.hashCode ^ + enabledIconColor.hashCode ^ + disabledIconColor.hashCode ^ + iconSize.hashCode; } /// [FButton] icon content's style. @@ -140,24 +185,52 @@ final class FButtonIconContentStyle with Diagnosticable { /// The padding. final EdgeInsets padding; + /// The icon's color when this button is enabled. + final Color enabled; + + /// The icon's color when this button is disabled. + final Color disabled; + + /// The icon's size. Defaults to 20. + final double size; + /// Creates a [FButtonIconContentStyle]. - const FButtonIconContentStyle({this.padding = const EdgeInsets.all(7.5)}); + const FButtonIconContentStyle({ + required this.enabled, + required this.disabled, + this.padding = const EdgeInsets.all(7.5), + this.size = 20, + }); /// Returns a copy of this [FButtonIconContentStyle] with the given properties replaced. @useResult - FButtonIconContentStyle copyWith({EdgeInsets? padding}) => FButtonIconContentStyle(padding: padding ?? this.padding); + FButtonIconContentStyle copyWith({EdgeInsets? padding}) => FButtonIconContentStyle( + padding: padding ?? this.padding, + enabled: enabled, + disabled: disabled, + size: size, + ); @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); - properties.add(DiagnosticsProperty('padding', padding)); + properties + ..add(DiagnosticsProperty('padding', padding)) + ..add(ColorProperty('enabledColor', enabled)) + ..add(ColorProperty('disabledColor', disabled)) + ..add(DoubleProperty('size', size, defaultValue: 20)); } @override bool operator ==(Object other) => identical(this, other) || - other is FButtonIconContentStyle && runtimeType == other.runtimeType && padding == other.padding; + other is FButtonIconContentStyle && + runtimeType == other.runtimeType && + padding == other.padding && + enabled == other.enabled && + disabled == other.disabled && + size == other.size; @override - int get hashCode => padding.hashCode; + int get hashCode => padding.hashCode ^ enabled.hashCode ^ disabled.hashCode ^ size.hashCode; } diff --git a/forui/lib/src/widgets/button/button_icon.dart b/forui/lib/src/widgets/button/button_icon.dart deleted file mode 100644 index 5e13d94f7..000000000 --- a/forui/lib/src/widgets/button/button_icon.dart +++ /dev/null @@ -1,89 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/widgets.dart'; - -import 'package:meta/meta.dart'; - -import 'package:forui/forui.dart'; - -/// A [FButton]'s icon. -class FButtonIcon extends StatelessWidget { - /// The icon. - final SvgAsset icon; - - /// Creates a [FButtonIcon] from the given SVG [icon]. - const FButtonIcon({required this.icon, super.key}); - - @override - Widget build(BuildContext context) { - final FButtonData(style: FButtonCustomStyle(:icon), :enabled) = FButtonData.of(context); - - return this.icon( - height: icon.size, - colorFilter: ColorFilter.mode(enabled ? icon.enabledColor : icon.disabledColor, BlendMode.srcIn), - ); - } - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add(DiagnosticsProperty('icon', icon)); - } -} - -/// [FButtonIcon]'s style. -final class FButtonIconStyle with Diagnosticable { - /// The icon's color when this button is enabled. - final Color enabledColor; - - /// The icon's color when this button is disabled. - final Color disabledColor; - - /// The icon's size. Defaults to 20. - final double size; - - /// Creates a [FButtonIconStyle]. - /// - /// ## Contract - /// Throws [AssertionError] if: - /// * `size` <= 0.0 - /// * `size` is Nan - FButtonIconStyle({ - required this.enabledColor, - required this.disabledColor, - this.size = 20, - }) : assert(0 < size, 'The size is $size, but it should be in the range "0 < size".'); - - /// Returns a copy of this [FBadgeContentStyle] with the given properties replaced. - @useResult - FButtonIconStyle copyWith({ - Color? enabledColor, - Color? disabledColor, - double? size, - }) => - FButtonIconStyle( - 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: 20)); - } - - @override - bool operator ==(Object other) => - identical(this, other) || - other is FButtonIconStyle && - runtimeType == other.runtimeType && - enabledColor == other.enabledColor && - disabledColor == other.disabledColor && - size == other.size; - - @override - int get hashCode => enabledColor.hashCode ^ disabledColor.hashCode ^ size.hashCode; -} diff --git a/forui/lib/src/widgets/button/button_styles.dart b/forui/lib/src/widgets/button/button_styles.dart index 2d4114541..ba28f95bf 100644 --- a/forui/lib/src/widgets/button/button_styles.dart +++ b/forui/lib/src/widgets/button/button_styles.dart @@ -48,9 +48,9 @@ final class FButtonStyles with Diagnosticable { enabled: colorScheme.primaryForeground, disabled: colorScheme.primaryForeground.withOpacity(0.5), ), - icon: FButtonIconStyle( - enabledColor: colorScheme.primaryForeground, - disabledColor: colorScheme.primaryForeground.withOpacity(0.5), + iconContent: FButtonIconContentStyle( + enabled: colorScheme.primaryForeground, + disabled: colorScheme.primaryForeground.withOpacity(0.5), ), ), secondary = FButtonCustomStyle( @@ -71,9 +71,9 @@ final class FButtonStyles with Diagnosticable { enabled: colorScheme.secondaryForeground, disabled: colorScheme.secondaryForeground.withOpacity(0.5), ), - icon: FButtonIconStyle( - enabledColor: colorScheme.secondaryForeground, - disabledColor: colorScheme.secondaryForeground.withOpacity(0.5), + iconContent: FButtonIconContentStyle( + enabled: colorScheme.secondaryForeground, + disabled: colorScheme.secondaryForeground.withOpacity(0.5), ), ), destructive = FButtonCustomStyle( @@ -94,9 +94,9 @@ final class FButtonStyles with Diagnosticable { enabled: colorScheme.destructiveForeground, disabled: colorScheme.destructiveForeground.withOpacity(0.5), ), - icon: FButtonIconStyle( - enabledColor: colorScheme.destructiveForeground, - disabledColor: colorScheme.destructiveForeground.withOpacity(0.5), + iconContent: FButtonIconContentStyle( + enabled: colorScheme.destructiveForeground, + disabled: colorScheme.destructiveForeground.withOpacity(0.5), ), ), outline = FButtonCustomStyle( @@ -120,9 +120,9 @@ final class FButtonStyles with Diagnosticable { enabled: colorScheme.secondaryForeground, disabled: colorScheme.secondaryForeground.withOpacity(0.5), ), - icon: FButtonIconStyle( - enabledColor: colorScheme.secondaryForeground, - disabledColor: colorScheme.secondaryForeground.withOpacity(0.5), + iconContent: FButtonIconContentStyle( + enabled: colorScheme.secondaryForeground, + disabled: colorScheme.secondaryForeground.withOpacity(0.5), ), ); diff --git a/forui/lib/src/widgets/header/header_action.dart b/forui/lib/src/widgets/header/header_action.dart index d18229e75..2ff24130f 100644 --- a/forui/lib/src/widgets/header/header_action.dart +++ b/forui/lib/src/widgets/header/header_action.dart @@ -11,7 +11,9 @@ class FHeaderAction extends StatelessWidget { final String? semanticLabel; /// The icon. - final SvgAsset icon; + /// + /// [icon] is wrapped in [FIconStyle], and therefore works with [FIcon]s. + final Widget icon; /// A callback for when the button is pressed. /// @@ -41,7 +43,7 @@ class FHeaderAction extends StatelessWidget { Key? key, }) => FHeaderAction( - icon: FAssets.icons.arrowLeft, + icon: FIcon(FAssets.icons.arrowLeft), onPress: onPress, style: style, semanticLabel: semanticLabel, @@ -55,7 +57,7 @@ class FHeaderAction extends StatelessWidget { Key? key, }) => FHeaderAction( - icon: FAssets.icons.x, + icon: FIcon(FAssets.icons.x), onPress: onPress, style: style, key: key, @@ -70,9 +72,12 @@ class FHeaderAction extends StatelessWidget { semanticLabel: semanticLabel, onPress: onPress, onLongPress: onLongPress, - child: icon( - height: style.size, - colorFilter: ColorFilter.mode(enabled ? style.enabledColor : style.disabledColor, BlendMode.srcIn), + child: FInheritedIconStyle( + style: FIconStyle( + color: enabled ? style.enabledColor : style.disabledColor, + size: style.size, + ), + child: icon, ), ); } diff --git a/forui/lib/src/widgets/icon.dart b/forui/lib/src/widgets/icon.dart new file mode 100644 index 000000000..2bd7ecf59 --- /dev/null +++ b/forui/lib/src/widgets/icon.dart @@ -0,0 +1,324 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'package:forui/forui.dart'; + +/// The [FIconStyle] that this [FInheritedIconStyle]'s widget subtree should inherit. +class FInheritedIconStyle extends InheritedWidget { + /// The icon's data. + final FIconStyle style; + + /// Creates a [FIconStyle]. + const FInheritedIconStyle({ + required this.style, + required super.child, + super.key, + }); + + @override + bool updateShouldNotify(FInheritedIconStyle old) => style != old.style; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('style', style)); + } +} + +/// The default properties of [FIcon]s in a widget subtree. +class FIconStyle with Diagnosticable { + /// The icon style from the closest instance of [FInheritedIconStyle] that encloses the given context, or + /// [FStyle.iconStyle] otherwise. + static FIconStyle of(BuildContext context) => + context.dependOnInheritedWidgetOfExactType()?.style ?? context.theme.style.iconStyle; + + /// The icon's color. + final Color color; + + /// The icon's size. + /// + /// ## Contract + /// Throws [AssertionError] if `size` is not positive. + final double size; + + /// Creates a [FIconStyle]. + const FIconStyle({ + required this.color, + required this.size, + }) : assert(0 < size, 'size is $size, but it should be positive.'); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(ColorProperty('color', color)) + ..add(DoubleProperty('size', size)); + } +} + +/// A graphical icon widget that inherits its style from an [FIconStyle], if any. +/// +/// [FIconStyle] allows icons to be automatically configured by an enclosing widget. It is explicitly mentioned in +/// a widget's documentation, such as [FButton.icon], if it provides an [FIconStyle]. [FIcon] defaults to +/// [FStyle.iconStyle] otherwise. +/// +/// See: +/// * [FIconStyle] for the properties that can be inherited. +/// * [FAssets.icons] for bundled Forui icons. +abstract class FIcon extends StatelessWidget { + /// The icon's color. + final Color? color; + + /// The icon's size. + /// + /// ## Contract + /// Throws [AssertionError] if `size` <= 0.0 + final double? size; + + /// The icon's semantic label. + final String? semanticLabel; + + /// Creates a [FIcon] from a [SvgAsset]. + const factory FIcon( + SvgAsset icon, { + bool matchTextDirection, + BoxFit fit, + AlignmentGeometry alignment, + bool allowDrawingOutsideViewBox, + WidgetBuilder? placeholderBuilder, + Clip clipBehavior, + Color color, + double size, + String? semanticLabel, + Key? key, + }) = _Icon; + + /// Creates a [FIcon] from an [IconData]. + /// + /// See [Icon] for more information. + const factory FIcon.data( + IconData data, { + double fill, + double weight, + double grade, + double opticalSize, + List shadows, + TextDirection textDirection, + bool applyTextScaling, + Color color, + double size, + String semanticLabel, + Key? key, + }) = _IconDataIcon; + + /// Creates a [FIcon] from an [ImageProvider]. + /// + /// **Note:** Provided images should always have a transparent background. Otherwise, the entire icon will be [color]. + /// + /// Set [color] to [Colors.transparent] to avoid recoloring the image. + /// + /// See [ImageIcon] for more information. + const factory FIcon.image( + ImageProvider image, { + Color? color, + double? size, + String? semanticLabel, + Key? key, + }) = _ImageProviderIcon; + + /// Creates a [FIcon] from a [ValueWidgetBuilder]. + /// + /// To access widget-specific data, i.e. [FButtonData] inside a [FButton]: + /// ```dart + /// FButton.icon( + /// icon: FIcon.raw( + /// builder: (context, style, _) { + /// final data = FButtonData.of(context); + /// final someWidget = ... // Create someWidget using data here + /// return someWidget; + /// }, + /// ); + /// ``` + const factory FIcon.raw({required ValueWidgetBuilder builder, Widget? child, Key? key}) = _BuilderIcon; + + const FIcon._({required this.color, this.size, this.semanticLabel, super.key}); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(ColorProperty('color', color)) + ..add(DoubleProperty('size', size)) + ..add(StringProperty('semanticLabel', semanticLabel)); + } +} + +class _Icon extends FIcon { + final SvgAsset icon; + final bool matchTextDirection; + final BoxFit fit; + final AlignmentGeometry alignment; + final bool allowDrawingOutsideViewBox; + final WidgetBuilder? placeholderBuilder; + final Clip clipBehavior; + + const _Icon( + this.icon, { + this.matchTextDirection = false, + this.fit = BoxFit.contain, + this.alignment = Alignment.center, + this.allowDrawingOutsideViewBox = false, + this.placeholderBuilder, + this.clipBehavior = Clip.hardEdge, + super.color, + super.size, + super.semanticLabel, + super.key, + }) : super._(); + + @override + Widget build(BuildContext context) { + final data = FIconStyle.of(context); + return icon.call( + matchTextDirection: matchTextDirection, + semanticsLabel: semanticLabel, + fit: fit, + alignment: alignment, + allowDrawingOutsideViewBox: allowDrawingOutsideViewBox, + placeholderBuilder: placeholderBuilder, + excludeFromSemantics: true, + clipBehavior: clipBehavior, + colorFilter: ColorFilter.mode(color ?? data.color, BlendMode.srcIn), + height: size ?? data.size, + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('icon', icon)) + ..add(FlagProperty('matchTextDirection', value: matchTextDirection, ifTrue: 'match text direction')) + ..add(EnumProperty('fit', fit)) + ..add(DiagnosticsProperty('alignment', alignment)) + ..add( + FlagProperty( + 'allowDrawingOutsideViewBox', + value: allowDrawingOutsideViewBox, + ifTrue: 'allow drawing outside view box', + ), + ) + ..add(ObjectFlagProperty.has('placeholderBuilder', placeholderBuilder)) + ..add(StringProperty('semanticLabel', semanticLabel)) + ..add(EnumProperty('clipBehavior', clipBehavior)); + } +} + +class _IconDataIcon extends FIcon { + final IconData icon; + final double? fill; + final double? weight; + final double? grade; + final double? opticalSize; + final List? shadows; + final TextDirection? textDirection; + final bool applyTextScaling; + + const _IconDataIcon( + this.icon, { + this.fill, + this.weight, + this.grade, + this.opticalSize, + this.shadows, + this.textDirection, + this.applyTextScaling = true, + super.color, + super.size, + super.semanticLabel, + super.key, + }) : super._(); + + @override + Widget build(BuildContext context) { + final data = FIconStyle.of(context); + return Icon( + icon, + fill: fill, + weight: weight, + grade: grade, + opticalSize: opticalSize, + shadows: shadows, + semanticLabel: semanticLabel, + textDirection: textDirection, + applyTextScaling: applyTextScaling, + color: color ?? data.color, + size: size ?? data.size, + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('icon', icon)) + ..add(DoubleProperty('fill', fill)) + ..add(DoubleProperty('weight', weight)) + ..add(DoubleProperty('grade', grade)) + ..add(DoubleProperty('opticalSize', opticalSize)) + ..add(IterableProperty('shadows', shadows)) + ..add(StringProperty('semanticLabel', semanticLabel)) + ..add(EnumProperty('direction', textDirection)) + ..add(FlagProperty('applyTextScaling', value: applyTextScaling, ifTrue: 'apply text scaling')); + } +} + +class _ImageProviderIcon extends FIcon { + final ImageProvider image; + + const _ImageProviderIcon( + this.image, { + super.color, + super.size, + super.semanticLabel, + super.key, + }) : super._(); + + @override + Widget build(BuildContext context) { + final data = FIconStyle.of(context); + return Semantics( + label: semanticLabel, + child: Image( + image: image, + height: size ?? data.size, + width: size ?? data.size, + color: color == Colors.transparent ? null : (color ?? data.color), + fit: BoxFit.scaleDown, + excludeFromSemantics: true, + ), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('image', image)); + } +} + +class _BuilderIcon extends FIcon { + final ValueWidgetBuilder builder; + final Widget? child; + + const _BuilderIcon({required this.builder, this.child, super.key}) : super._(color: null, size: null); + + @override + Widget build(BuildContext context) => builder(context, FIconStyle.of(context), child); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(ObjectFlagProperty.has('builder', builder)); + } +} diff --git a/forui/lib/widgets/alert.dart b/forui/lib/widgets/alert.dart index 41d406f60..c1879c0df 100644 --- a/forui/lib/widgets/alert.dart +++ b/forui/lib/widgets/alert.dart @@ -5,6 +5,4 @@ /// See https://forui.dev/docs/alert for working examples. library forui.widgets.alert; -export '../src/widgets/alert/alert.dart' hide InheritedData, Variant; -export '../src/widgets/alert/alert_icon.dart'; -export '../src/widgets/alert/alert_styles.dart'; +export '../src/widgets/alert.dart' hide Variant; diff --git a/forui/lib/widgets/button.dart b/forui/lib/widgets/button.dart index 3bd9c4941..d4bd9edf0 100644 --- a/forui/lib/widgets/button.dart +++ b/forui/lib/widgets/button.dart @@ -7,5 +7,4 @@ library forui.widgets.button; export '../src/widgets/button/button.dart' hide Variant; export '../src/widgets/button/button_content.dart' hide Content, IconContent; -export '../src/widgets/button/button_icon.dart'; export '../src/widgets/button/button_styles.dart'; diff --git a/forui/lib/widgets/icon.dart b/forui/lib/widgets/icon.dart new file mode 100644 index 000000000..19a3ebe05 --- /dev/null +++ b/forui/lib/widgets/icon.dart @@ -0,0 +1,8 @@ +/// {@category Widgets} +/// +/// An icon that may inherit its style from its enclosing widget if supported. +/// +/// See https://forui.dev/docs/icon for working examples. +library forui.widgets.icon; + +export '../src/widgets/icon.dart'; diff --git a/forui/test/golden/icon/icon-data-zinc-dark.png b/forui/test/golden/icon/icon-data-zinc-dark.png new file mode 100644 index 000000000..ff2820d85 Binary files /dev/null and b/forui/test/golden/icon/icon-data-zinc-dark.png differ diff --git a/forui/test/golden/icon/icon-data-zinc-light.png b/forui/test/golden/icon/icon-data-zinc-light.png new file mode 100644 index 000000000..b5a709351 Binary files /dev/null and b/forui/test/golden/icon/icon-data-zinc-light.png differ diff --git a/forui/test/golden/icon/icon-style.png b/forui/test/golden/icon/icon-style.png new file mode 100644 index 000000000..cb7bbc8ba Binary files /dev/null and b/forui/test/golden/icon/icon-style.png differ diff --git a/forui/test/golden/icon/image-original-zinc-dark.png b/forui/test/golden/icon/image-original-zinc-dark.png new file mode 100644 index 000000000..ec7e2b437 Binary files /dev/null and b/forui/test/golden/icon/image-original-zinc-dark.png differ diff --git a/forui/test/golden/icon/image-original-zinc-light.png b/forui/test/golden/icon/image-original-zinc-light.png new file mode 100644 index 000000000..3f28b468e Binary files /dev/null and b/forui/test/golden/icon/image-original-zinc-light.png differ diff --git a/forui/test/golden/icon/image-recolored-zinc-dark.png b/forui/test/golden/icon/image-recolored-zinc-dark.png new file mode 100644 index 000000000..70b1159b1 Binary files /dev/null and b/forui/test/golden/icon/image-recolored-zinc-dark.png differ diff --git a/forui/test/golden/icon/image-recolored-zinc-light.png b/forui/test/golden/icon/image-recolored-zinc-light.png new file mode 100644 index 000000000..e1086f56d Binary files /dev/null and b/forui/test/golden/icon/image-recolored-zinc-light.png differ diff --git a/forui/test/golden/icon/raw-zinc-dark.png b/forui/test/golden/icon/raw-zinc-dark.png new file mode 100644 index 000000000..5aa407f59 Binary files /dev/null and b/forui/test/golden/icon/raw-zinc-dark.png differ diff --git a/forui/test/golden/icon/raw-zinc-light.png b/forui/test/golden/icon/raw-zinc-light.png new file mode 100644 index 000000000..1a1dd23c7 Binary files /dev/null and b/forui/test/golden/icon/raw-zinc-light.png differ diff --git a/forui/test/golden/icon/svg-asset-zinc-dark.png b/forui/test/golden/icon/svg-asset-zinc-dark.png new file mode 100644 index 000000000..0b50611dc Binary files /dev/null and b/forui/test/golden/icon/svg-asset-zinc-dark.png differ diff --git a/forui/test/golden/icon/svg-asset-zinc-light.png b/forui/test/golden/icon/svg-asset-zinc-light.png new file mode 100644 index 000000000..b3f0851e7 Binary files /dev/null and b/forui/test/golden/icon/svg-asset-zinc-light.png differ diff --git a/forui/test/resources/forus-labs.png b/forui/test/resources/forus-labs.png new file mode 100644 index 000000000..f69658a2f Binary files /dev/null and b/forui/test/resources/forus-labs.png differ diff --git a/forui/test/src/widgets/alert/alert_icon_test.dart b/forui/test/src/widgets/alert/alert_icon_test.dart deleted file mode 100644 index b94e50592..000000000 --- a/forui/test/src/widgets/alert/alert_icon_test.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:flutter_test/flutter_test.dart'; - -import 'package:forui/forui.dart'; - -void main() { - group('FAlertIconStyle', () { - test('invalid height', () { - expect( - () => FAlertIconStyle( - size: 0, - color: Colors.white, - ), - throwsAssertionError, - ); - }); - - test('valid height', () { - expect( - () => FAlertIconStyle( - size: 1, - color: Colors.white, - ), - returnsNormally, - ); - }); - }); -} diff --git a/forui/test/src/widgets/alert/alert_golden_test.dart b/forui/test/src/widgets/alert_golden_test.dart similarity index 91% rename from forui/test/src/widgets/alert/alert_golden_test.dart rename to forui/test/src/widgets/alert_golden_test.dart index a2ff1a053..e3cd5b263 100644 --- a/forui/test/src/widgets/alert/alert_golden_test.dart +++ b/forui/test/src/widgets/alert_golden_test.dart @@ -6,8 +6,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:forui/forui.dart'; -import 'package:forui/src/widgets/alert/alert.dart'; -import '../../test_scaffold.dart'; +import 'package:forui/src/widgets/alert.dart'; +import '../test_scaffold.dart'; void main() { group('FAlert', () { @@ -41,7 +41,7 @@ void main() { child: Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: FAlert( - icon: FAlertIcon(icon: FAssets.icons.badgeAlert), + icon: FIcon(FAssets.icons.badgeAlert), title: const Text('Alert Title'), subtitle: const Text('Alert description with extra text'), style: variant, diff --git a/forui/test/src/widgets/avatar_golden_test.dart b/forui/test/src/widgets/avatar_golden_test.dart index 4c90f00b5..7e5be8f04 100644 --- a/forui/test/src/widgets/avatar_golden_test.dart +++ b/forui/test/src/widgets/avatar_golden_test.dart @@ -37,6 +37,7 @@ void main() { await tester.pumpAndSettle(); } }); + await expectLater( find.byType(TestScaffold), matchesGoldenFile('avatar/$name-with-image.png'), @@ -45,7 +46,6 @@ void main() { /// We will not be testing for the fallback behavior due to this issue on flutter /// https://github.com/flutter/flutter/issues/107416 - testWidgets('$name with raw content', (tester) async { await tester.pumpWidget( TestScaffold( diff --git a/forui/test/src/widgets/bottom_navigation_bar_golden_test.dart b/forui/test/src/widgets/bottom_navigation_bar_golden_test.dart index 28d3bcfd1..d04ae1413 100644 --- a/forui/test/src/widgets/bottom_navigation_bar_golden_test.dart +++ b/forui/test/src/widgets/bottom_navigation_bar_golden_test.dart @@ -20,23 +20,23 @@ void main() { index: 2, children: [ FBottomNavigationBarItem( - icon: FAssets.icons.home, + icon: FIcon(FAssets.icons.home), label: const Text('Home'), ), FBottomNavigationBarItem( - icon: FAssets.icons.layoutGrid, + icon: FIcon(FAssets.icons.layoutGrid), label: const Text('Browse'), ), FBottomNavigationBarItem( - icon: FAssets.icons.radio, + icon: FIcon(FAssets.icons.radio), label: const Text('Radio'), ), FBottomNavigationBarItem( - icon: FAssets.icons.libraryBig, + icon: FIcon(FAssets.icons.radio), label: const Text('Library'), ), FBottomNavigationBarItem( - icon: FAssets.icons.search, + icon: FIcon(FAssets.icons.radio), label: const Text('Search'), ), ], @@ -49,51 +49,6 @@ void main() { matchesGoldenFile('bottom-navigation-bar/forui-icon-$name.png'), ); }); - - testWidgets('custom icon - $name', (tester) async { - Widget icon(BuildContext _, FBottomNavigationBarData data, Widget? __) => Container( - height: data.itemStyle.iconSize, - width: data.itemStyle.iconSize, - color: data.selected ? data.itemStyle.activeIconColor : data.itemStyle.inactiveIconColor, - ); - - await tester.pumpWidget( - TestScaffold( - data: theme, - background: background, - child: FBottomNavigationBar( - index: 2, - children: [ - FBottomNavigationBarItem.custom( - iconBuilder: icon, - label: const Text('Home'), - ), - FBottomNavigationBarItem.custom( - iconBuilder: icon, - label: const Text('Browse'), - ), - FBottomNavigationBarItem.custom( - iconBuilder: icon, - label: const Text('Radio'), - ), - FBottomNavigationBarItem.custom( - iconBuilder: icon, - label: const Text('Library'), - ), - FBottomNavigationBarItem.custom( - iconBuilder: icon, - label: const Text('Search'), - ), - ], - ), - ), - ); - - await expectLater( - find.byType(TestScaffold), - matchesGoldenFile('bottom-navigation-bar/custom-icon-$name.png'), - ); - }); } }); } diff --git a/forui/test/src/widgets/button/button_golden_test.dart b/forui/test/src/widgets/button/button_golden_test.dart index fa5c969b9..e1970b1b6 100644 --- a/forui/test/src/widgets/button/button_golden_test.dart +++ b/forui/test/src/widgets/button/button_golden_test.dart @@ -23,8 +23,9 @@ void main() { child: FButton( label: const Text('Button'), style: variant, - prefix: FButtonIcon(icon: FAssets.icons.circlePlay), - suffix: FButtonIcon(icon: FAssets.icons.circleStop), + // We don't have access to widget specific fields :( + prefix: FIcon(FAssets.icons.circlePlay), + suffix: FIcon(FAssets.icons.circleStop), onPress: () {}, ), ), @@ -46,8 +47,8 @@ void main() { child: FButton( label: const Text('Button'), style: variant, - prefix: FButtonIcon(icon: FAssets.icons.circlePlay), - suffix: FButtonIcon(icon: FAssets.icons.circleStop), + prefix: FIcon(FAssets.icons.circlePlay), + suffix: FIcon(FAssets.icons.circleStop), onPress: () {}, ), ), @@ -77,8 +78,8 @@ void main() { child: FButton( label: const Text('Button'), style: variant, - prefix: FButtonIcon(icon: FAssets.icons.circlePlay), - suffix: FButtonIcon(icon: FAssets.icons.circleStop), + prefix: FIcon(FAssets.icons.circlePlay), + suffix: FIcon(FAssets.icons.circleStop), onPress: () {}, ), ), @@ -108,8 +109,8 @@ void main() { child: FButton( label: const Text('Button'), style: variant, - prefix: FButtonIcon(icon: FAssets.icons.circlePlay), - suffix: FButtonIcon(icon: FAssets.icons.circleStop), + prefix: FIcon(FAssets.icons.circlePlay), + suffix: FIcon(FAssets.icons.circleStop), onPress: null, ), ), @@ -209,9 +210,7 @@ void main() { child: FButton.icon( onPress: () {}, style: variant, - child: FButtonIcon( - icon: FAssets.icons.chevronRight, - ), + child: FIcon(FAssets.icons.chevronRight), ), ), ), @@ -234,9 +233,7 @@ void main() { child: FButton.icon( onPress: null, style: variant, - child: FButtonIcon( - icon: FAssets.icons.chevronRight, - ), + child: FIcon(FAssets.icons.chevronRight), ), ), ), diff --git a/forui/test/src/widgets/button/button_icon_test.dart b/forui/test/src/widgets/button/button_icon_test.dart deleted file mode 100644 index ef394e8e2..000000000 --- a/forui/test/src/widgets/button/button_icon_test.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:flutter_test/flutter_test.dart'; - -import 'package:forui/forui.dart'; - -void main() { - group('FButtonIconStyle', () { - test('invalid height', () { - expect( - () => FButtonIconStyle( - size: 0, - enabledColor: Colors.white, - disabledColor: Colors.white, - ), - throwsAssertionError, - ); - }); - - test('valid height', () { - expect( - () => FButtonIconStyle( - size: 1, - enabledColor: Colors.white, - disabledColor: Colors.white, - ), - returnsNormally, - ); - }); - }); -} diff --git a/forui/test/src/widgets/header/nested_header_golden_test.dart b/forui/test/src/widgets/header/nested_header_golden_test.dart index bca8ffa74..9a8911280 100644 --- a/forui/test/src/widgets/header/nested_header_golden_test.dart +++ b/forui/test/src/widgets/header/nested_header_golden_test.dart @@ -22,13 +22,13 @@ void main() { leftActions: [ FHeaderAction.back(onPress: () {}), FHeaderAction( - icon: FAssets.icons.alarmClock, + icon: FIcon(FAssets.icons.alarmClock), onPress: null, ), ], rightActions: [ FHeaderAction( - icon: FAssets.icons.plus, + icon: FIcon(FAssets.icons.plus), onPress: () {}, ), FHeaderAction.x(onPress: () {}), diff --git a/forui/test/src/widgets/header/root_header_golden_test.dart b/forui/test/src/widgets/header/root_header_golden_test.dart index bc1645ad4..43c6ab024 100644 --- a/forui/test/src/widgets/header/root_header_golden_test.dart +++ b/forui/test/src/widgets/header/root_header_golden_test.dart @@ -24,11 +24,11 @@ void main() { title: const Text(title), actions: [ FHeaderAction( - icon: FAssets.icons.alarmClock, + icon: FIcon(FAssets.icons.alarmClock), onPress: null, ), FHeaderAction( - icon: FAssets.icons.plus, + icon: FIcon(FAssets.icons.plus), onPress: () {}, ), ], diff --git a/forui/test/src/widgets/icon_golden_test.dart b/forui/test/src/widgets/icon_golden_test.dart new file mode 100644 index 000000000..6d5916815 --- /dev/null +++ b/forui/test/src/widgets/icon_golden_test.dart @@ -0,0 +1,109 @@ +@Tags(['golden']) +library; + +import 'dart:io'; + +import 'package:flutter/material.dart'; + +import 'package:flutter_test/flutter_test.dart'; + +import 'package:forui/forui.dart'; +import '../test_scaffold.dart'; + +void main() { + group('FIcon', () { + testWidgets('with parent IconStyle', (tester) async { + await tester.pumpWidget( + TestScaffold( + data: FThemes.zinc.light, + child: FInheritedIconStyle( + style: const FIconStyle(color: Colors.red, size: 48), + child: FIcon(FAssets.icons.laugh), + ), + ), + ); + + await expectLater(find.byType(TestScaffold), matchesGoldenFile('icon/icon-style.png')); + }); + + for (final (name, theme, _) in TestScaffold.themes) { + testWidgets('$name with SvgAsset', (tester) async { + await tester.pumpWidget( + TestScaffold( + data: theme, + child: FIcon(FAssets.icons.laugh), + ), + ); + + await expectLater(find.byType(TestScaffold), matchesGoldenFile('icon/svg-asset-$name.png')); + }); + + testWidgets('$name with IconData', (tester) async { + await tester.pumpWidget( + TestScaffold( + data: theme, + // This will always be a square since we don't include material's icon in the pubspec.yaml. + child: const FIcon.data(Icons.add), + ), + ); + + await expectLater(find.byType(TestScaffold), matchesGoldenFile('icon/icon-data-$name.png')); + }); + + testWidgets('$name with ImageProvider', (tester) async { + final image = TestScaffold( + data: theme, + child: FIcon.image(FileImage(File('./test/resources/forus-labs.png'))), + ); + + await tester.runAsync(() async { + await tester.pumpWidget(image); + for (final element in find.byType(Image).evaluate()) { + final Image widget = element.widget as Image; + final ImageProvider image = widget.image; + await precacheImage(image, element); + await tester.pumpAndSettle(); + } + }); + + await expectLater(find.byType(TestScaffold), matchesGoldenFile('icon/image-recolored-$name.png')); + }); + + testWidgets('$name with ImageProvider and no recoloring', (tester) async { + final image = TestScaffold( + data: theme, + child: FIcon.image(FileImage(File('./test/resources/forus-labs.png')), color: Colors.transparent), + ); + + await tester.runAsync(() async { + await tester.pumpWidget(image); + for (final element in find.byType(Image).evaluate()) { + final Image widget = element.widget as Image; + final ImageProvider image = widget.image; + await precacheImage(image, element); + await tester.pumpAndSettle(); + } + }); + + await expectLater(find.byType(TestScaffold), matchesGoldenFile('icon/image-original-$name.png')); + }); + + testWidgets('$name with raw builder', (tester) async { + await tester.pumpWidget( + TestScaffold( + data: theme, + child: FIcon.raw( + builder: (context, style, _) => Container( + color: style.color, + height: style.size, + width: style.size, + ), + ), + ), + ); + + await expectLater(find.byType(TestScaffold), matchesGoldenFile('icon/raw-$name.png')); + }); + } + }); +} diff --git a/samples/assets/forus-labs.png b/samples/assets/forus-labs.png new file mode 100644 index 000000000..f69658a2f Binary files /dev/null and b/samples/assets/forus-labs.png differ diff --git a/samples/lib/main.dart b/samples/lib/main.dart index 708d19c20..c937285ea 100644 --- a/samples/lib/main.dart +++ b/samples/lib/main.dart @@ -43,7 +43,6 @@ class _AppRouter extends RootStackRouter { AutoRoute(path: '/avatar/invalid', page: AvatarInvalidRoute.page), AutoRoute(path: '/badge/default', page: BadgeRoute.page), AutoRoute(path: '/bottom-navigation-bar/default', page: BottomNavigationBarRoute.page), - AutoRoute(path: '/bottom-navigation-bar/custom', page: CustomBottomNavigationBarRoute.page), AutoRoute(path: '/button/text', page: ButtonTextRoute.page), AutoRoute(path: '/button/icon', page: ButtonIconRoute.page), AutoRoute(path: '/button/only-icon', page: ButtonOnlyIconRoute.page), @@ -60,6 +59,10 @@ class _AppRouter extends RootStackRouter { AutoRoute(path: '/header/default', page: RootHeaderRoute.page), AutoRoute(path: '/header/nested', page: NestedHeaderRoute.page), AutoRoute(path: '/header/nested-x', page: XNestedHeaderRoute.page), + AutoRoute(path: '/icon/default', page: IconRoute.page), + AutoRoute(path: '/icon/comparison', page: ComparisonIconRoute.page), + AutoRoute(path: '/icon/custom', page: CustomIconRoute.page), + AutoRoute(path: '/icon/image', page: ImageIconRoute.page), AutoRoute(path: '/label/vertical', page: VerticalLabelRoute.page), AutoRoute(path: '/label/horizontal', page: HorizontalLabelRoute.page), AutoRoute(path: '/popover/default', page: PopoverRoute.page), diff --git a/samples/lib/widgets/alert.dart b/samples/lib/widgets/alert.dart index f8b4f87e4..d674ba1aa 100644 --- a/samples/lib/widgets/alert.dart +++ b/samples/lib/widgets/alert.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:auto_route/auto_route.dart'; -import 'package:forui/src/widgets/alert/alert.dart'; +import 'package:forui/src/widgets/alert.dart'; import 'package:forui_samples/sample_scaffold.dart'; diff --git a/samples/lib/widgets/bottom_navigation_bar.dart b/samples/lib/widgets/bottom_navigation_bar.dart index 434ce1407..3f2643180 100644 --- a/samples/lib/widgets/bottom_navigation_bar.dart +++ b/samples/lib/widgets/bottom_navigation_bar.dart @@ -34,95 +34,23 @@ class _DemoState extends State<_Demo> { onChange: (index) => setState(() => this.index = index), children: [ FBottomNavigationBarItem( - icon: FAssets.icons.home, + icon: FIcon(FAssets.icons.home), label: const Text('Home'), ), FBottomNavigationBarItem( - icon: FAssets.icons.layoutGrid, + icon: FIcon(FAssets.icons.layoutGrid), label: const Text('Browse'), ), FBottomNavigationBarItem( - icon: FAssets.icons.radio, + icon: FIcon(FAssets.icons.radio), label: const Text('Radio'), ), FBottomNavigationBarItem( - icon: FAssets.icons.libraryBig, + icon: FIcon(FAssets.icons.libraryBig), label: const Text('Library'), ), FBottomNavigationBarItem( - icon: FAssets.icons.search, - label: const Text('Search'), - ), - ], - ); -} - -@RoutePage() -class CustomBottomNavigationBarPage extends SampleScaffold { - CustomBottomNavigationBarPage({ - @queryParam super.theme, - }); - - @override - Widget child(BuildContext context) => const Padding( - padding: EdgeInsets.all(15.0), - child: _CustomDemo(), - ); -} - -class _CustomDemo extends StatefulWidget { - const _CustomDemo(); - - @override - State<_CustomDemo> createState() => _CustomDemoState(); -} - -class _CustomDemoState extends State<_CustomDemo> { - int index = 1; - - @override - Widget build(BuildContext context) => FBottomNavigationBar( - index: index, - onChange: (index) => setState(() => this.index = index), - children: [ - FBottomNavigationBarItem.custom( - iconBuilder: (_, data, __) => Icon( - Icons.home_outlined, - size: data.itemStyle.iconSize, - color: data.selected ? data.itemStyle.activeIconColor : data.itemStyle.inactiveIconColor, - ), - label: const Text('Home'), - ), - FBottomNavigationBarItem.custom( - iconBuilder: (_, data, __) => Icon( - Icons.browse_gallery_outlined, - size: data.itemStyle.iconSize, - color: data.selected ? data.itemStyle.activeIconColor : data.itemStyle.inactiveIconColor, - ), - label: const Text('Browse'), - ), - FBottomNavigationBarItem.custom( - iconBuilder: (_, data, __) => Icon( - Icons.radio_outlined, - size: data.itemStyle.iconSize, - color: data.selected ? data.itemStyle.activeIconColor : data.itemStyle.inactiveIconColor, - ), - label: const Text('Radio'), - ), - FBottomNavigationBarItem.custom( - iconBuilder: (_, data, __) => Icon( - Icons.library_books_outlined, - size: data.itemStyle.iconSize, - color: data.selected ? data.itemStyle.activeIconColor : data.itemStyle.inactiveIconColor, - ), - label: const Text('Library'), - ), - FBottomNavigationBarItem.custom( - iconBuilder: (_, data, __) => Icon( - Icons.search_outlined, - size: data.itemStyle.iconSize, - color: data.selected ? data.itemStyle.activeIconColor : data.itemStyle.inactiveIconColor, - ), + icon: FIcon(FAssets.icons.search), label: const Text('Search'), ), ], diff --git a/samples/lib/widgets/button.dart b/samples/lib/widgets/button.dart index fa5d6dc8b..bcf7532b7 100644 --- a/samples/lib/widgets/button.dart +++ b/samples/lib/widgets/button.dart @@ -45,7 +45,7 @@ class ButtonIconPage extends SampleScaffold { @override Widget child(BuildContext context) => IntrinsicWidth( child: FButton( - prefix: FButtonIcon(icon: FAssets.icons.mail), + prefix: FIcon(FAssets.icons.mail), label: const Text('Login with Email'), style: variant, onPress: () {}, @@ -62,7 +62,7 @@ class ButtonOnlyIconPage extends SampleScaffold { @override Widget child(BuildContext context) => IntrinsicWidth( child: FButton.icon( - child: FButtonIcon(icon: FAssets.icons.chevronRight), + child: FIcon(FAssets.icons.chevronRight), onPress: () {}, ), ); diff --git a/samples/lib/widgets/header.dart b/samples/lib/widgets/header.dart index 41412d02c..613ab6f22 100644 --- a/samples/lib/widgets/header.dart +++ b/samples/lib/widgets/header.dart @@ -16,11 +16,11 @@ class RootHeaderPage extends SampleScaffold { title: const Text('Edit Alarm'), actions: [ FHeaderAction( - icon: FAssets.icons.alarmClock, + icon: FIcon(FAssets.icons.alarmClock), onPress: () {}, ), FHeaderAction( - icon: FAssets.icons.plus, + icon: FIcon(FAssets.icons.plus), onPress: () {}, ), ], @@ -41,11 +41,11 @@ class NestedHeaderPage extends SampleScaffold { ], rightActions: [ FHeaderAction( - icon: FAssets.icons.info, + icon: FIcon(FAssets.icons.info), onPress: () {}, ), FHeaderAction( - icon: FAssets.icons.plus, + icon: FIcon(FAssets.icons.plus), onPress: () {}, ), ], @@ -63,11 +63,11 @@ class XNestedHeaderPage extends SampleScaffold { title: const Text('Climate'), leftActions: [ FHeaderAction( - icon: FAssets.icons.thermometer, + icon: FIcon(FAssets.icons.thermometer), onPress: () {}, ), FHeaderAction( - icon: FAssets.icons.wind, + icon: FIcon(FAssets.icons.wind), onPress: null, ), ], diff --git a/samples/lib/widgets/icon.dart b/samples/lib/widgets/icon.dart new file mode 100644 index 000000000..8c8d68e9f --- /dev/null +++ b/samples/lib/widgets/icon.dart @@ -0,0 +1,142 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'package:auto_route/auto_route.dart'; +import 'package:forui/forui.dart'; + +import 'package:forui_samples/sample_scaffold.dart'; + +String path(String str) => kIsWeb ? 'assets/$str' : str; + +@RoutePage() +class IconPage extends SampleScaffold { + final String variant; + + IconPage({ + @queryParam super.theme, + @queryParam this.variant = 'svg', + }); + + @override + Widget child(BuildContext context) => IntrinsicWidth( + child: FButton.icon( + style: FButtonStyle.secondary, + child: switch (variant) { + 'data' => const FIcon.data(Icons.wifi), + _ => FIcon(FAssets.icons.wifi), + }, + onPress: () {}, + ), + ); +} + +@RoutePage() +class ComparisonIconPage extends SampleScaffold { + ComparisonIconPage({ + @queryParam super.theme, + }); + + @override + Widget child(BuildContext context) => Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FButton.icon( + style: FButtonStyle.primary, + child: FIcon(FAssets.icons.bird), + onPress: () {}, + ), + const SizedBox(width: 10), + FButton.icon( + style: FButtonStyle.secondary, + child: FIcon(FAssets.icons.bird), + onPress: () {}, + ), + ], + ); +} + +@RoutePage() +class ImageIconPage extends SampleScaffold { + ImageIconPage({ + @queryParam super.theme, + }); + + @override + Widget child(BuildContext context) => Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FButton.icon( + style: FButtonStyle.primary, + child: FIcon.image(AssetImage(path('forus-labs.png')), color: Colors.transparent), + onPress: () {}, + ), + const SizedBox(width: 10), + FButton.icon( + style: FButtonStyle.primary, + child: FIcon.image(AssetImage(path('forus-labs.png'))), + onPress: () {}, + ), + ], + ); +} + +@RoutePage() +class CustomIconPage extends SampleScaffold { + CustomIconPage({ + @queryParam super.theme, + }); + + @override + Widget child(BuildContext context) => IntrinsicWidth( + child: FButton.icon( + child: FIcon.raw( + builder: (context, style, child) { + final FButtonData(:enabled) = FButtonData.of(context); + return enabled ? _Icon(style: style) : const FIcon.data(Icons.menu); + }, + ), + onPress: () {}, + ), + ); +} + +class _Icon extends StatefulWidget { + final FIconStyle style; + + const _Icon({required this.style}); + + @override + State<_Icon> createState() => _IconState(); +} + +class _IconState extends State<_Icon> with SingleTickerProviderStateMixin { + late AnimationController controller; + late Animation animation; + + @override + void initState() { + super.initState(); + controller = AnimationController( + vsync: this, + duration: const Duration(seconds: 3), + ) + ..forward() + ..repeat(reverse: true); + animation = Tween(begin: 0.0, end: 1.0).animate(controller); + } + + @override + Widget build(BuildContext context) => AnimatedIcon( + icon: AnimatedIcons.home_menu, + progress: animation, + color: widget.style.color, + size: widget.style.size, + semanticLabel: 'Home menu', + ); + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } +} diff --git a/samples/lib/widgets/scaffold.dart b/samples/lib/widgets/scaffold.dart index 217ea121f..cc417ac31 100644 --- a/samples/lib/widgets/scaffold.dart +++ b/samples/lib/widgets/scaffold.dart @@ -31,7 +31,7 @@ final headers = [ title: const Text('Settings'), actions: [ FHeaderAction( - icon: FAssets.icons.ellipsis, + icon: FIcon(FAssets.icons.ellipsis), onPress: () {}, ), ], @@ -99,19 +99,19 @@ class _DemoState extends State<_Demo> { onChange: (index) => setState(() => this.index = index), children: [ FBottomNavigationBarItem( - icon: FAssets.icons.home, + icon: FIcon(FAssets.icons.home), label: const Text('Home'), ), FBottomNavigationBarItem( - icon: FAssets.icons.layoutGrid, + icon: FIcon(FAssets.icons.layoutGrid), label: const Text('Categories'), ), FBottomNavigationBarItem( - icon: FAssets.icons.search, + icon: FIcon(FAssets.icons.search), label: const Text('Search'), ), FBottomNavigationBarItem( - icon: FAssets.icons.settings, + icon: FIcon(FAssets.icons.settings), label: const Text('Settings'), ), ], diff --git a/samples/pubspec.lock b/samples/pubspec.lock index 24be77b54..33fd75b2c 100644 --- a/samples/pubspec.lock +++ b/samples/pubspec.lock @@ -98,10 +98,10 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: dd09dd4e2b078992f42aac7f1a622f01882a8492fef08486b27ddde929c19f04 + sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" url: "https://pub.dev" source: hosted - version: "2.4.12" + version: "2.4.13" build_runner_core: dependency: transitive description: @@ -436,10 +436,10 @@ packages: dependency: transitive description: name: mime - sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" url: "https://pub.dev" source: hosted - version: "1.0.6" + version: "2.0.0" nitrogen_flutter_svg: dependency: transitive description: @@ -737,10 +737,10 @@ packages: dependency: transitive description: name: web - sha256: d43c1d6b787bf0afad444700ae7f4db8827f701bc61c255ac8d328c6f4d52062 + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.0" web_socket: dependency: transitive description: diff --git a/samples/pubspec.yaml b/samples/pubspec.yaml index 3a33a0c86..79b21a714 100644 --- a/samples/pubspec.yaml +++ b/samples/pubspec.yaml @@ -75,6 +75,7 @@ flutter: assets: - assets/avatar.png + - assets/forus-labs.png # To add custom fonts to your application, add a fonts section here, # in this "flutter" section. Each entry in this list should have a