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..300a7abde 100644 --- a/docs/pages/docs/icon.mdx +++ b/docs/pages/docs/icon.mdx @@ -1,49 +1,214 @@ -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', +); ``` +### `FIcon.data(...)` -## Usage +```dart +FIcon.data( + Icons.abc, + color: Colors.red, + size: 24, + semanticLabel: 'Label', +); +``` - - 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). - +### `FIcon.image(...)` ```dart -import 'package:forui/forui.dart'; - -// alternatively; if you've only installed forui_assets. -import 'package:forui_assets/forui_assets.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 {23-34} + 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 0551a5534..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`. @@ -46,6 +44,10 @@ * * **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 048634819..bf706ca5f 100644 --- a/forui/lib/foundation.dart +++ b/forui/lib/foundation.dart @@ -1,7 +1,6 @@ /// Low-level utilities and services. library forui.foundation; -export 'src/foundation/icon.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/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 69% rename from forui/lib/src/widgets/alert/alert.dart rename to forui/lib/src/widgets/alert.dart index f106293c9..534e0d4b2 100644 --- a/forui/lib/src/widgets/alert/alert.dart +++ b/forui/lib/src/widgets/alert.dart @@ -15,7 +15,7 @@ import 'package:forui/forui.dart'; class FAlert extends StatelessWidget { /// The icon. Defaults to [FAssets.icons.circleAlert]. /// - /// Icons are wrapped in [FIconData], and therefore works with [FIcon]s. + /// [icon] is wrapped in [FIconStyle], and therefore works with [FIcon]s. final Widget icon; /// The title. @@ -63,9 +63,8 @@ class FAlert extends StatelessWidget { children: [ Row( children: [ - FIconData( - color: style.iconColor, - size: style.iconSize, + FInheritedIconStyle( + style: FIconStyle(color: style.iconColor, size: style.iconSize), child: icon, ), Flexible( @@ -107,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 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 ef75dab5a..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), - 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; -} 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 b1139ca4c..ed3ae3a43 100644 --- a/forui/lib/src/widgets/button/button.dart +++ b/forui/lib/src/widgets/button/button.dart @@ -62,7 +62,7 @@ class FButton extends StatelessWidget { /// Creates a [FButton] that contains a [prefix], [label], and [suffix]. /// - /// [prefix] and [suffix] are wrapped in [FIconData], and therefore works with [FIcon]s. + /// [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: /// ``` @@ -84,6 +84,8 @@ class FButton extends StatelessWidget { }) : 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, diff --git a/forui/lib/src/widgets/button/button_content.dart b/forui/lib/src/widgets/button/button_content.dart index 5fa4102d8..086735f0c 100644 --- a/forui/lib/src/widgets/button/button_content.dart +++ b/forui/lib/src/widgets/button/button_content.dart @@ -27,9 +27,11 @@ class Content extends StatelessWidget { padding: content.padding, child: DefaultTextStyle.merge( style: enabled ? content.enabledTextStyle : content.disabledTextStyle, - child: FIconData( - color: enabled ? content.enabledIconColor : content.disabledIconColor, - size: content.iconSize, + child: FInheritedIconStyle( + style: FIconStyle( + color: enabled ? content.enabledIconColor : content.disabledIconColor, + size: content.iconSize, + ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: separate( @@ -61,9 +63,11 @@ class IconContent extends StatelessWidget { return Padding( padding: style.iconContent.padding, - child: FIconData( - color: enabled ? style.iconContent.enabled : style.iconContent.disabled, - size: style.iconContent.size, + child: FInheritedIconStyle( + style: FIconStyle( + color: enabled ? style.iconContent.enabled : style.iconContent.disabled, + size: style.iconContent.size, + ), child: child, ), ); 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/foundation/icon.dart b/forui/lib/src/widgets/icon.dart similarity index 61% rename from forui/lib/src/foundation/icon.dart rename to forui/lib/src/widgets/icon.dart index 487e67b88..505d9ae4b 100644 --- a/forui/lib/src/foundation/icon.dart +++ b/forui/lib/src/widgets/icon.dart @@ -1,11 +1,35 @@ import 'package:flutter/foundation.dart'; -import 'package:flutter/widgets.dart'; -import 'package:forui/assets.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 FIconData extends InheritedWidget { - /// The data from the closest instance of this class that encloses the given context, if any. - static FIconData? maybeOf(BuildContext context) => context.dependOnInheritedWidgetOfExactType(); +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; @@ -16,17 +40,12 @@ class FIconData extends InheritedWidget { /// Throws [AssertionError] if `size` is not positive. final double size; - /// Creates a [FIconData]. - const FIconData({ + /// Creates a [FIconStyle]. + const FIconStyle({ required this.color, required this.size, - required super.child, - super.key, }) : assert(0 < size, 'size is $size, but it should be positive.'); - @override - bool updateShouldNotify(FIconData old) => color != old.color || size != old.size; - @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); @@ -36,7 +55,15 @@ class FIconData extends InheritedWidget { } } -/// A graphical icon widget that inherits from a [FIconData], if any. +/// 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; @@ -86,15 +113,35 @@ abstract class FIcon extends StatelessWidget { /// 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, + 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 @@ -134,12 +181,17 @@ class _Icon extends FIcon { @override Widget build(BuildContext context) { - final data = FIconData.maybeOf(context); - final color = this.color ?? data?.color; - + final data = FIconStyle.of(context); return icon.call( - colorFilter: color == null ? null : ColorFilter.mode(color, BlendMode.srcIn), - height: size ?? data?.size, + matchTextDirection: matchTextDirection, + fit: fit, + alignment: alignment, + allowDrawingOutsideViewBox: allowDrawingOutsideViewBox, + placeholderBuilder: placeholderBuilder, + excludeFromSemantics: excludeFromSemantics, + clipBehavior: clipBehavior, + colorFilter: ColorFilter.mode(color ?? data.color, BlendMode.srcIn), + height: size ?? data.size, ); } @@ -172,7 +224,6 @@ class _IconDataIcon extends FIcon { final double? grade; final double? opticalSize; final List? shadows; - final TextDirection? textDirection; final bool applyTextScaling; @@ -193,8 +244,7 @@ class _IconDataIcon extends FIcon { @override Widget build(BuildContext context) { - final data = FIconData.maybeOf(context); - final color = this.color ?? data?.color; + final data = FIconStyle.of(context); return Icon( icon, fill: fill, @@ -205,8 +255,8 @@ class _IconDataIcon extends FIcon { semanticLabel: semanticLabel, textDirection: textDirection, applyTextScaling: applyTextScaling, - color: color, - size: size ?? data?.size, + color: color ?? data.color, + size: size ?? data.size, ); } @@ -239,12 +289,17 @@ class _ImageProviderIcon extends FIcon { @override Widget build(BuildContext context) { - final data = FIconData.maybeOf(context); - return ImageIcon( - image, - color: color ?? data?.color, - size: size ?? data?.size, - semanticLabel: semanticLabel, + 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, + ), ); } @@ -254,3 +309,19 @@ class _ImageProviderIcon extends FIcon { 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 bb07bac69..c1879c0df 100644 --- a/forui/lib/widgets/alert.dart +++ b/forui/lib/widgets/alert.dart @@ -5,5 +5,4 @@ /// See https://forui.dev/docs/alert for working examples. library forui.widgets.alert; -export '../src/widgets/alert/alert.dart' hide Variant; -export '../src/widgets/alert/alert_styles.dart'; +export '../src/widgets/alert.dart' hide Variant; 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_golden_test.dart b/forui/test/src/widgets/alert_golden_test.dart similarity index 95% rename from forui/test/src/widgets/alert/alert_golden_test.dart rename to forui/test/src/widgets/alert_golden_test.dart index 68eea6e02..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', () { 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 2d6a032cd..e1970b1b6 100644 --- a/forui/test/src/widgets/button/button_golden_test.dart +++ b/forui/test/src/widgets/button/button_golden_test.dart @@ -23,6 +23,7 @@ void main() { child: FButton( label: const Text('Button'), style: variant, + // We don't have access to widget specific fields :( prefix: FIcon(FAssets.icons.circlePlay), suffix: FIcon(FAssets.icons.circleStop), onPress: () {}, 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..812a81bce --- /dev/null +++ b/forui/test/src/widgets/icon_golden_test.dart @@ -0,0 +1,107 @@ +@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/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..61cff6c8f --- /dev/null +++ b/samples/lib/widgets/icon.dart @@ -0,0 +1,135 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.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