diff --git a/docs/pages/docs/index.mdx b/docs/pages/docs/index.mdx index 4025e8618..3ed5fca51 100644 --- a/docs/pages/docs/index.mdx +++ b/docs/pages/docs/index.mdx @@ -35,7 +35,7 @@ To use Forui widgets in your Flutter app, import the Forui package and place the [`FTheme`](https://pub.dev/documentation/forui/latest/forui.theme/FTheme-class.html) widget underneath `CupertinoApp`, `MaterialApp`, or `WidgetsApp` at at the root of the widget tree. -```dart filename="main.dart" {3,12-16} +```dart filename="main.dart" {3,12-18} import 'package:flutter/material.dart'; import 'package:forui/forui.dart'; diff --git a/docs/pages/docs/slider.mdx b/docs/pages/docs/slider.mdx index a65274215..2f04cc4d6 100644 --- a/docs/pages/docs/slider.mdx +++ b/docs/pages/docs/slider.mdx @@ -49,15 +49,36 @@ FSlider( ### Appearance +#### Labelled + + + + + + + ```dart {2-3} + FSlider( + label: const Text('Volume'), + description: const Text('Adjust the volume by dragging the slider.'), + controller: FContinuousSliderController( + selection: FSliderSelection(max: 0.6), + ), + ); + ``` + + + #### Disabled - + - ```dart {5} + ```dart {7} FSlider( + label: const Text('Volume'), + description: const Text('Adjust the volume by dragging the slider.'), controller: FContinuousSliderController( selection: FSliderSelection(max: 0.6), ), @@ -67,6 +88,26 @@ FSlider( +#### Error + + + + + + + ```dart {4} + FSlider( + label: const Text('Volume'), + description: const Text('Adjust the volume by dragging the slider.'), + forceErrorText: 'Volume is too high.', + controller: FContinuousSliderController( + selection: FSliderSelection(max: 0.6), + ), + ); + ``` + + + #### Tooltip @@ -183,23 +224,24 @@ describes the mark's value. - + - ```dart {2} + ```dart {4} FSlider( + label: const Text('Volume'), + description: const Text('Adjust the volume by dragging the slider.'), layout: Layout.btt, - controller: FContinuousSliderController( - selection: FSliderSelection(max: 0.6), - ), + controller: FContinuousSliderController(selection: FSliderSelection(max: 0.35)), + trackMainAxisExtent: 350, marks: const [ FSliderMark(value: 0, label: Text('0%')), FSliderMark(value: 0.25, tick: false), - FSliderMark(value: 0.5), + FSliderMark(value: 0.5, label: Text('50%')), FSliderMark(value: 0.75, tick: false), FSliderMark(value: 1, label: Text('100%')), ], - ); + ), ``` @@ -285,3 +327,64 @@ Tap anywhere or slide the thumb to select a value. ``` + +### Form + + + + + + + ```dart + class SliderForm extends StatefulWidget { + const SliderForm({super.key}); + + @override + State createState() => SliderFormState(); + } + + class SliderFormState extends State { + final GlobalKey _formKey = GlobalKey(); + + @override + Widget build(BuildContext context) => Form( + key: _formKey, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FSlider( + label: const Text('Brightness'), + description: const Text('Adjust the brightness level.'), + // validator: (value) => 'An error text.', + controller: FContinuousSliderController( + selection: FSliderSelection(max: 0.35), + ), + marks: const [ + FSliderMark(value: 0, label: Text('0%')), + FSliderMark(value: 0.25, tick: false), + FSliderMark(value: 0.5, label: Text('50%')), + FSliderMark(value: 0.75, tick: false), + FSliderMark(value: 1, label: Text('100%')), + ], + ), + const SizedBox(height: 20), + FButton( + label: const Text('Save'), + onPress: () { + if (!_formKey.currentState!.validate()) { + // Handle errors here. + return; + } + + _formKey.currentState!.save(); + // Do something. + }, + ), + ], + ), + ); + } + ``` + + diff --git a/forui/CHANGELOG.md b/forui/CHANGELOG.md index 501ad6755..e6c7c940b 100644 --- a/forui/CHANGELOG.md +++ b/forui/CHANGELOG.md @@ -13,6 +13,9 @@ * Add `FTextField.forceErrorText`. +* **Breaking** Add `FColorScheme.disabledColorBrightness` - this will only affect users that create a `FColorScheme` + from scratch. + ### Changes * Change button to change color when hovering over it. @@ -30,6 +33,10 @@ * **Breaking** Remove `FTextFieldErrorStyle.animatioDuration`. +* **Breaking** Rename `FLabelStateStyle` to `FLabelStateStyles`. + +* **Breaking** Rename `FTextField.onSave` to `FTextField.onSaved`. + ### Fixes * Fix `FBottomNavigationBar` items hit region being smaller than intended. @@ -46,6 +53,8 @@ * Fix `FSwitch` not using correct label style. +* Fix `FCheckbox`, `FRadio`, `FSelectGroup`, `FSwitch` and `FTextField` styles causing the widget inspector to crash. + ## 0.5.1 diff --git a/forui/example/ios/Podfile.lock b/forui/example/ios/Podfile.lock index 422d97c17..da9928701 100644 --- a/forui/example/ios/Podfile.lock +++ b/forui/example/ios/Podfile.lock @@ -1,22 +1,34 @@ PODS: - Flutter (1.0.0) + - package_info_plus (0.4.5): + - Flutter - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS + - wakelock_plus (0.0.1): + - Flutter DEPENDENCIES: - Flutter (from `Flutter`) + - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) + - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`) EXTERNAL SOURCES: Flutter: :path: Flutter + package_info_plus: + :path: ".symlinks/plugins/package_info_plus/ios" path_provider_foundation: :path: ".symlinks/plugins/path_provider_foundation/darwin" + wakelock_plus: + :path: ".symlinks/plugins/wakelock_plus/ios" SPEC CHECKSUMS: Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1 PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796 diff --git a/forui/example/lib/main.dart b/forui/example/lib/main.dart index 2a594337d..914bf0283 100644 --- a/forui/example/lib/main.dart +++ b/forui/example/lib/main.dart @@ -3,8 +3,12 @@ import 'package:flutter/material.dart'; import 'package:forui/forui.dart'; import 'package:forui_example/sandbox.dart'; +import 'package:wakelock_plus/wakelock_plus.dart'; void main() { + WidgetsFlutterBinding.ensureInitialized(); + WakelockPlus.enable(); + runApp(const Application()); } @@ -24,7 +28,7 @@ class Application extends StatefulWidget { } class _ApplicationState extends State { - int index = 0; + int index = 4; @override Widget build(BuildContext context) => MaterialApp( diff --git a/forui/example/lib/sandbox.dart b/forui/example/lib/sandbox.dart index a579264b0..4668ee3a8 100644 --- a/forui/example/lib/sandbox.dart +++ b/forui/example/lib/sandbox.dart @@ -19,16 +19,7 @@ class _SandboxState extends State { } @override - Widget build(BuildContext context) => Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - FTextField.email( - autovalidateMode: AutovalidateMode.always, - description: const Text('Description'), - validator: (value) => value?.length == 5 ? 'Error message' : null, - ), - const SizedBox(height: 20), - const FTextField.password(), - ], + Widget build(BuildContext context) => FSlider( + controller: FContinuousSliderController.range(selection: FSliderSelection(min: 0.30, max: 0.35)), ); } diff --git a/forui/example/macos/Podfile.lock b/forui/example/macos/Podfile.lock index a83eba5b7..45334275b 100644 --- a/forui/example/macos/Podfile.lock +++ b/forui/example/macos/Podfile.lock @@ -1,22 +1,34 @@ PODS: - FlutterMacOS (1.0.0) + - package_info_plus (0.0.1): + - FlutterMacOS - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS + - wakelock_plus (0.0.1): + - FlutterMacOS DEPENDENCIES: - FlutterMacOS (from `Flutter/ephemeral`) + - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) + - wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`) EXTERNAL SOURCES: FlutterMacOS: :path: Flutter/ephemeral + package_info_plus: + :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos path_provider_foundation: :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin + wakelock_plus: + :path: Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos SPEC CHECKSUMS: FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 + package_info_plus: fa739dd842b393193c5ca93c26798dff6e3d0e0c path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269 PODFILE CHECKSUM: 236401fc2c932af29a9fcf0e97baeeb2d750d367 diff --git a/forui/example/pubspec.lock b/forui/example/pubspec.lock index 3569e2b1e..3c651e518 100644 --- a/forui/example/pubspec.lock +++ b/forui/example/pubspec.lock @@ -174,6 +174,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.7" + dbus: + dependency: transitive + description: + name: dbus + sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" + url: "https://pub.dev" + source: hosted + version: "0.7.10" fake_async: dependency: transitive description: @@ -237,6 +245,11 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" forui: dependency: "direct main" description: @@ -444,6 +457,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + package_info_plus: + dependency: transitive + description: + name: package_info_plus + sha256: a75164ade98cb7d24cfd0a13c6408927c6b217fa60dee5a7ff5c116a58f28918 + url: "https://pub.dev" + source: hosted + version: "8.0.2" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: ac1f4a4847f1ade8e6a87d1f39f5d7c67490738642e2542f559ec38c37489a66 + url: "https://pub.dev" + source: hosted + version: "3.0.1" path: dependency: transitive description: @@ -705,6 +734,22 @@ packages: url: "https://pub.dev" source: hosted version: "14.2.5" + wakelock_plus: + dependency: "direct main" + description: + name: wakelock_plus + sha256: bf4ee6f17a2fa373ed3753ad0e602b7603f8c75af006d5b9bdade263928c0484 + url: "https://pub.dev" + source: hosted + version: "1.2.8" + wakelock_plus_platform_interface: + dependency: transitive + description: + name: wakelock_plus_platform_interface + sha256: "422d1cdbb448079a8a62a5a770b69baa489f8f7ca21aef47800c726d404f9d16" + url: "https://pub.dev" + source: hosted + version: "1.2.1" watcher: dependency: transitive description: @@ -717,10 +762,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: @@ -737,6 +782,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.1" + win32: + dependency: transitive + description: + name: win32 + sha256: "68d1e89a91ed61ad9c370f9f8b6effed9ae5e0ede22a270bdfa6daf79fc2290a" + url: "https://pub.dev" + source: hosted + version: "5.5.4" xdg_directories: dependency: transitive description: diff --git a/forui/example/pubspec.yaml b/forui/example/pubspec.yaml index f4cfe9daa..b29afe800 100644 --- a/forui/example/pubspec.yaml +++ b/forui/example/pubspec.yaml @@ -29,6 +29,7 @@ dependencies: path: ../ intl: ^0.19.0 sugar: ^3.1.0 + wakelock_plus: ^1.2.8 dev_dependencies: build_runner: ^2.4.0 diff --git a/forui/lib/src/foundation/platform.dart b/forui/lib/src/foundation/platform.dart deleted file mode 100644 index 337085b98..000000000 --- a/forui/lib/src/foundation/platform.dart +++ /dev/null @@ -1,8 +0,0 @@ -import 'package:flutter/foundation.dart'; - -import 'package:meta/meta.dart'; - -@internal - -/// The platforms that are primarily touch enabled. It isn't 100% accurate but it is a good approximation. -const touchPlatforms = {TargetPlatform.android, TargetPlatform.iOS, TargetPlatform.fuchsia}; diff --git a/forui/lib/src/foundation/portal/portal_shift.dart b/forui/lib/src/foundation/portal/portal_shift.dart index 71c2ad954..1efb3912a 100644 --- a/forui/lib/src/foundation/portal/portal_shift.dart +++ b/forui/lib/src/foundation/portal/portal_shift.dart @@ -1,7 +1,7 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; -import 'package:forui/src/foundation/alignment.dart'; +import 'package:forui/src/foundation/rendering.dart'; /// A portal's target. typedef FPortalTarget = ({Offset offset, Size size, Alignment anchor}); diff --git a/forui/lib/src/foundation/alignment.dart b/forui/lib/src/foundation/rendering.dart similarity index 90% rename from forui/lib/src/foundation/alignment.dart rename to forui/lib/src/foundation/rendering.dart index ef89ea007..9c987942f 100644 --- a/forui/lib/src/foundation/alignment.dart +++ b/forui/lib/src/foundation/rendering.dart @@ -1,7 +1,12 @@ -import 'package:flutter/widgets.dart'; +import 'package:flutter/rendering.dart'; import 'package:meta/meta.dart'; +@internal +extension RenderBoxes on RenderBox { + BoxParentData get data => parentData! as BoxParentData; +} + @internal extension Alignments on Alignment { Alignment flipX() => switch (this) { diff --git a/forui/lib/src/foundation/tappable.dart b/forui/lib/src/foundation/tappable.dart index 0bec9299b..13e45a19d 100644 --- a/forui/lib/src/foundation/tappable.dart +++ b/forui/lib/src/foundation/tappable.dart @@ -1,13 +1,36 @@ import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:meta/meta.dart'; -// TODO: Remove redundant comment when flutter fixes its lint issue. +@internal +extension Touch on Never { + /// The platforms that uses touch as the primary input. It isn't 100% accurate as there are hybrid devices that uses + /// both touch and keyboard/mouse input, i.e. Windows Surface laptops. + static const platforms = {TargetPlatform.android, TargetPlatform.iOS, TargetPlatform.fuchsia}; + + static bool? _primary; + + /// True if the current platform uses touch as the primary input. + static bool get primary => _primary ?? platforms.contains(defaultTargetPlatform); + + @visibleForTesting + static set primary(bool? value) { + if (!kDebugMode) { + throw UnsupportedError('Setting Touch.primary is only available in debug mode.'); + } + + _primary = value; + } +} + +/// The [FTappableState]'s current state. /// +/// Short pressed is fired 200 ms after a tap is first detected. This is faster than long press's [kLongPressTimeout]. @internal -typedef FTappableState = ({bool focused, bool hovered, bool longPressed}); +typedef FTappableState = ({bool focused, bool hovered, bool shortPressed}); @internal class FTappable extends StatefulWidget { @@ -89,7 +112,7 @@ class _FTappableState extends State { int _monotonic = 0; bool _focused = false; bool _hovered = false; - bool _longPressed = false; + bool _shortPressed = false; @override Widget build(BuildContext context) { @@ -119,13 +142,13 @@ class _FTappableState extends State { final count = ++_monotonic; await Future.delayed(const Duration(milliseconds: 200)); if (count == _monotonic) { - setState(() => _longPressed = true); + setState(() => _shortPressed = true); } }, onPointerUp: (_) { _monotonic++; - if (_longPressed) { - setState(() => _longPressed = false); + if (_shortPressed) { + setState(() => _shortPressed = false); } }, child: _child, @@ -155,7 +178,8 @@ class _FTappableState extends State { behavior: widget.behavior, onTap: widget.onPress, onLongPress: widget.onLongPress, - child: widget.builder(context, (focused: _focused, hovered: _hovered, longPressed: _longPressed), widget.child), + child: + widget.builder(context, (focused: _focused, hovered: _hovered, shortPressed: _shortPressed), widget.child), ); } @@ -207,8 +231,11 @@ class _AnimatedTappableState extends _FTappableState with SingleTickerProviderSt _controller.forward(); }, onLongPress: widget.onLongPress, - child: - widget.builder(context, (focused: _focused, hovered: _hovered, longPressed: _longPressed), widget.child), + child: widget.builder( + context, + (focused: _focused, hovered: _hovered, shortPressed: _shortPressed), + widget.child, + ), ), ); diff --git a/forui/lib/src/theme/color_scheme.dart b/forui/lib/src/theme/color_scheme.dart index 21e94662b..bfc1778ec 100644 --- a/forui/lib/src/theme/color_scheme.dart +++ b/forui/lib/src/theme/color_scheme.dart @@ -27,6 +27,9 @@ final class FColorScheme with Diagnosticable { /// This is typically used to determine the appearance of native UI elements such as on-screen keyboards. final Brightness brightness; + /// The percentage, between `0` and `1`, used to set a color's lightness to derive a disabled color. + final double disabledColorLightness; + /// The background color. /// /// Typically used as a background for [foreground] colored widgets. @@ -96,6 +99,7 @@ final class FColorScheme with Diagnosticable { /// Unless you are creating a completely new color scheme, modifying [FThemes]' predefined color schemes is preferred. const FColorScheme({ required this.brightness, + required this.disabledColorLightness, required this.background, required this.foreground, required this.primary, @@ -111,6 +115,12 @@ final class FColorScheme with Diagnosticable { required this.border, }); + /// Creates a disabled color from the given [color]. + /// + /// See: + /// * [disabledColorLightness]. + Color disable(Color color) => HSLColor.fromColor(color).withLightness(disabledColorLightness).toColor(); + /// Returns a copy of this [FColorScheme] with the given properties replaced. /// /// ```dart @@ -128,6 +138,7 @@ final class FColorScheme with Diagnosticable { @useResult FColorScheme copyWith({ Brightness? brightness, + double? disabledColorLightness, Color? background, Color? foreground, Color? primary, @@ -144,6 +155,7 @@ final class FColorScheme with Diagnosticable { }) => FColorScheme( brightness: brightness ?? this.brightness, + disabledColorLightness: disabledColorLightness ?? this.disabledColorLightness, background: background ?? this.background, foreground: foreground ?? this.foreground, primary: primary ?? this.primary, @@ -164,6 +176,7 @@ final class FColorScheme with Diagnosticable { super.debugFillProperties(properties); properties ..add(EnumProperty('brightness', brightness)) + ..add(DoubleProperty('disabledColorLightness', disabledColorLightness)) ..add(ColorProperty('background', background)) ..add(ColorProperty('foreground', foreground)) ..add(ColorProperty('primary', primary)) @@ -184,6 +197,7 @@ final class FColorScheme with Diagnosticable { identical(this, other) || other is FColorScheme && brightness == other.brightness && + disabledColorLightness == other.disabledColorLightness && background == other.background && foreground == other.foreground && primary == other.primary && @@ -201,6 +215,7 @@ final class FColorScheme with Diagnosticable { @override int get hashCode => brightness.hashCode ^ + disabledColorLightness.hashCode ^ background.hashCode ^ foreground.hashCode ^ primary.hashCode ^ diff --git a/forui/lib/src/theme/themes.dart b/forui/lib/src/theme/themes.dart index e5ef2e8cf..260fd908a 100644 --- a/forui/lib/src/theme/themes.dart +++ b/forui/lib/src/theme/themes.dart @@ -10,6 +10,7 @@ extension FThemes on Never { light: FThemeData.inherit( colorScheme: const FColorScheme( brightness: Brightness.light, + disabledColorLightness: 0.4, background: Color(0xFFFFFFFF), foreground: Color(0xFF09090B), primary: Color(0xFF18181B), @@ -28,6 +29,7 @@ extension FThemes on Never { dark: FThemeData.inherit( colorScheme: const FColorScheme( brightness: Brightness.dark, + disabledColorLightness: 0.8, background: Color(0xFF09090B), foreground: Color(0xFFFAFAFA), primary: Color(0xFFFAFAFA), @@ -50,6 +52,7 @@ extension FThemes on Never { light: FThemeData.inherit( colorScheme: const FColorScheme( brightness: Brightness.light, + disabledColorLightness: 0.4, background: Color(0xFFFFFFFF), foreground: Color(0xFF020817), primary: Color(0xFF0F172A), @@ -68,6 +71,7 @@ extension FThemes on Never { dark: FThemeData.inherit( colorScheme: const FColorScheme( brightness: Brightness.dark, + disabledColorLightness: 0.8, background: Color(0xFF020817), foreground: Color(0xFFF8FAFC), primary: Color(0xFFF8FAFC), @@ -90,6 +94,7 @@ extension FThemes on Never { light: FThemeData.inherit( colorScheme: const FColorScheme( brightness: Brightness.light, + disabledColorLightness: 0.4, background: Color(0xFFFFFFFF), foreground: Color(0xFF09090B), primary: Color(0xFFDC2626), @@ -108,6 +113,7 @@ extension FThemes on Never { dark: FThemeData.inherit( colorScheme: const FColorScheme( brightness: Brightness.dark, + disabledColorLightness: 0.8, background: Color(0xFF0A0A0A), foreground: Color(0xFFFAFAFA), primary: Color(0xFFDC2626), @@ -130,6 +136,7 @@ extension FThemes on Never { light: FThemeData.inherit( colorScheme: const FColorScheme( brightness: Brightness.light, + disabledColorLightness: 0.4, background: Color(0xFFFFFFFF), foreground: Color(0xFF09090B), primary: Color(0xFFE11D48), @@ -148,6 +155,7 @@ extension FThemes on Never { dark: FThemeData.inherit( colorScheme: const FColorScheme( brightness: Brightness.dark, + disabledColorLightness: 0.8, background: Color(0xFF0C0A09), foreground: Color(0xFFF2F2F2), primary: Color(0xFFE11D48), @@ -170,6 +178,7 @@ extension FThemes on Never { light: FThemeData.inherit( colorScheme: const FColorScheme( brightness: Brightness.light, + disabledColorLightness: 0.4, background: Color(0xFFFFFFFF), foreground: Color(0xFF0C0A09), primary: Color(0xFFF97316), @@ -188,6 +197,7 @@ extension FThemes on Never { dark: FThemeData.inherit( colorScheme: const FColorScheme( brightness: Brightness.dark, + disabledColorLightness: 0.8, background: Color(0xFF0C0A09), foreground: Color(0xFFFAFAF9), primary: Color(0xFFEA580C), @@ -210,6 +220,7 @@ extension FThemes on Never { light: FThemeData.inherit( colorScheme: const FColorScheme( brightness: Brightness.light, + disabledColorLightness: 0.4, background: Color(0xFFFFFFFF), foreground: Color(0xFF09090B), primary: Color(0xFF16A34A), @@ -228,6 +239,7 @@ extension FThemes on Never { dark: FThemeData.inherit( colorScheme: const FColorScheme( brightness: Brightness.dark, + disabledColorLightness: 0.8, background: Color(0xFF0C0A09), foreground: Color(0xFFF2F2F2), primary: Color(0xFF22C55E), @@ -250,6 +262,7 @@ extension FThemes on Never { light: FThemeData.inherit( colorScheme: const FColorScheme( brightness: Brightness.light, + disabledColorLightness: 0.4, background: Color(0xFFFFFFFF), foreground: Color(0xFF020817), primary: Color(0xFF2563EB), @@ -268,6 +281,7 @@ extension FThemes on Never { dark: FThemeData.inherit( colorScheme: const FColorScheme( brightness: Brightness.dark, + disabledColorLightness: 0.8, background: Color(0xFF020817), foreground: Color(0xFFF8FAFC), primary: Color(0xFF3B82F6), @@ -290,6 +304,7 @@ extension FThemes on Never { light: FThemeData.inherit( colorScheme: const FColorScheme( brightness: Brightness.light, + disabledColorLightness: 0.4, background: Color(0xFFFFFFFF), foreground: Color(0xFF0C0A09), primary: Color(0xFFFACC15), @@ -308,6 +323,7 @@ extension FThemes on Never { dark: FThemeData.inherit( colorScheme: const FColorScheme( brightness: Brightness.dark, + disabledColorLightness: 0.8, background: Color(0xFF0C0A09), foreground: Color(0xFFFAFAF9), primary: Color(0xFFFACC15), @@ -330,6 +346,7 @@ extension FThemes on Never { light: FThemeData.inherit( colorScheme: const FColorScheme( brightness: Brightness.light, + disabledColorLightness: 0.4, background: Color(0xFFFFFFFF), foreground: Color(0xFF030712), primary: Color(0xFF7C3AED), @@ -348,6 +365,7 @@ extension FThemes on Never { dark: FThemeData.inherit( colorScheme: const FColorScheme( brightness: Brightness.dark, + disabledColorLightness: 0.8, background: Color(0xFF030712), foreground: Color(0xFFF9FAFB), primary: Color(0xFF6D28D9), diff --git a/forui/lib/src/widgets/button/button.dart b/forui/lib/src/widgets/button/button.dart index fc651c3aa..7655bd49b 100644 --- a/forui/lib/src/widgets/button/button.dart +++ b/forui/lib/src/widgets/button/button.dart @@ -130,7 +130,7 @@ class FButton extends StatelessWidget { onPress: onPress, onLongPress: onLongPress, builder: (context, state, child) => DecoratedBox( - decoration: switch ((enabled, state.hovered || state.longPressed)) { + decoration: switch ((enabled, state.hovered || state.shortPressed)) { (true, false) => style.enabledBoxDecoration, (true, true) => style.enabledHoverBoxDecoration, (false, _) => style.disabledBoxDecoration, diff --git a/forui/lib/src/widgets/calendar/shared/entry.dart b/forui/lib/src/widgets/calendar/shared/entry.dart index a17490311..92ed906ed 100644 --- a/forui/lib/src/widgets/calendar/shared/entry.dart +++ b/forui/lib/src/widgets/calendar/shared/entry.dart @@ -158,7 +158,7 @@ class _UnselectableEntry extends Entry { @override Widget build(BuildContext context) => ExcludeSemantics( - child: builder(context, (focused: false, hovered: false, longPressed: false), null), + child: builder(context, (focused: false, hovered: false, shortPressed: false), null), ); } @@ -179,7 +179,7 @@ class _Content extends StatelessWidget { @override Widget build(BuildContext context) { - final hovered = state.hovered || state.longPressed; + final hovered = state.hovered || state.shortPressed; var textStyle = hovered ? style.hoveredTextStyle : style.textStyle; if (current) { textStyle = textStyle.copyWith(decoration: TextDecoration.underline); diff --git a/forui/lib/src/widgets/checkbox.dart b/forui/lib/src/widgets/checkbox.dart index 69e18531f..57aafb6c8 100644 --- a/forui/lib/src/widgets/checkbox.dart +++ b/forui/lib/src/widgets/checkbox.dart @@ -210,9 +210,10 @@ class FCheckboxStyle with Diagnosticable { ); /// The [FLabel]'s style. + // ignore: diagnostic_describe_all_properties FLabelStyle get labelStyle => ( layout: labelLayoutStyle, - state: FLabelStateStyle( + state: FLabelStateStyles( enabledStyle: enabledStyle, disabledStyle: disabledStyle, errorStyle: errorStyle, @@ -247,8 +248,7 @@ class FCheckboxStyle with Diagnosticable { ..add(DiagnosticsProperty('labelLayoutStyle', labelLayoutStyle)) ..add(DiagnosticsProperty('enabledStyle', enabledStyle)) ..add(DiagnosticsProperty('disabledStyle', disabledStyle)) - ..add(DiagnosticsProperty('errorStyle', errorStyle)) - ..add(DiagnosticsProperty('labelStyle', labelStyle)); + ..add(DiagnosticsProperty('errorStyle', errorStyle)); } @override diff --git a/forui/lib/src/widgets/label.dart b/forui/lib/src/widgets/label.dart index 3d9acdf98..d45ec673f 100644 --- a/forui/lib/src/widgets/label.dart +++ b/forui/lib/src/widgets/label.dart @@ -6,7 +6,7 @@ import 'package:meta/meta.dart'; import 'package:forui/forui.dart'; /// The [FLabel]'s style. -typedef FLabelStyle = ({FLabelLayoutStyle layout, FLabelStateStyle state}); +typedef FLabelStyle = ({FLabelLayoutStyle layout, FLabelStateStyles state}); /// The label's state. enum FLabelState { @@ -112,7 +112,7 @@ final class FLabel extends StatelessWidget { error: error, state: state, child: child, - ) + ), }; } @@ -335,7 +335,7 @@ final class FLabelStyles with Diagnosticable { descriptionPadding: EdgeInsets.only(top: 2), errorPadding: EdgeInsets.only(top: 2), ), - state: FLabelStateStyle.inherit(style: style), + state: FLabelStateStyles.inherit(style: style), ), vertical = ( layout: const FLabelLayoutStyle( @@ -343,7 +343,7 @@ final class FLabelStyles with Diagnosticable { descriptionPadding: EdgeInsets.only(top: 5), errorPadding: EdgeInsets.only(top: 5), ), - state: FLabelStateStyle.inherit(style: style) + state: FLabelStateStyles.inherit(style: style) ); /// Returns a copy of this [FLabelStyles] with the given properties replaced. @@ -439,8 +439,8 @@ final class FLabelLayoutStyle with Diagnosticable { labelPadding.hashCode ^ descriptionPadding.hashCode ^ errorPadding.hashCode ^ childPadding.hashCode; } -/// The [FLabel]'s state style. -class FLabelStateStyle with Diagnosticable { +/// The [FLabel]'s state styles. +class FLabelStateStyles with Diagnosticable { /// The style for the form field when it is enabled. final FFormFieldStyle enabledStyle; @@ -450,15 +450,15 @@ class FLabelStateStyle with Diagnosticable { /// The style for the form field when it has an error. final FFormFieldErrorStyle errorStyle; - /// Creates a [FLabelStateStyle]. - FLabelStateStyle({ + /// Creates a [FLabelStateStyles]. + FLabelStateStyles({ required this.enabledStyle, required this.disabledStyle, required this.errorStyle, }); - /// Creates a [FLabelStateStyle] that inherits its properties from [style]. - FLabelStateStyle.inherit({required FStyle style}) + /// Creates a [FLabelStateStyles] that inherits its properties from [style]. + FLabelStateStyles.inherit({required FStyle style}) : enabledStyle = style.enabledFormFieldStyle, disabledStyle = style.disabledFormFieldStyle, errorStyle = style.errorFormFieldStyle; @@ -475,7 +475,7 @@ class FLabelStateStyle with Diagnosticable { @override bool operator ==(Object other) => identical(this, other) || - other is FLabelStateStyle && + other is FLabelStateStyles && runtimeType == other.runtimeType && enabledStyle == other.enabledStyle && disabledStyle == other.disabledStyle && diff --git a/forui/lib/src/widgets/popover.dart b/forui/lib/src/widgets/popover.dart index 939341ef4..80b0b6b4f 100644 --- a/forui/lib/src/widgets/popover.dart +++ b/forui/lib/src/widgets/popover.dart @@ -5,7 +5,7 @@ import 'package:flutter/widgets.dart'; import 'package:meta/meta.dart'; import 'package:forui/forui.dart'; -import 'package:forui/src/foundation/platform.dart'; +import 'package:forui/src/foundation/tappable.dart'; /// A controller that controls whether a [FPopover] is shown or hidden. final class FPopoverController extends ChangeNotifier { @@ -70,7 +70,7 @@ final class FPopoverController extends ChangeNotifier { /// * [FPopoverController] for controlling a popover. /// * [FPopoverStyle] for customizing a popover's appearance. class FPopover extends StatefulWidget { - static ({Alignment follower, Alignment target}) get _platform => touchPlatforms.contains(defaultTargetPlatform) + static ({Alignment follower, Alignment target}) get _platform => Touch.primary ? (follower: Alignment.bottomCenter, target: Alignment.topCenter) : (follower: Alignment.topCenter, target: Alignment.bottomCenter); diff --git a/forui/lib/src/widgets/radio.dart b/forui/lib/src/widgets/radio.dart index 26b99e5ba..40086c6a3 100644 --- a/forui/lib/src/widgets/radio.dart +++ b/forui/lib/src/widgets/radio.dart @@ -203,9 +203,11 @@ class FRadioStyle with Diagnosticable { ); /// The [FLabel]'s style. + + // ignore: diagnostic_describe_all_properties FLabelStyle get labelStyle => ( layout: labelLayoutStyle, - state: FLabelStateStyle( + state: FLabelStateStyles( enabledStyle: enabledStyle, disabledStyle: disabledStyle, errorStyle: errorStyle, @@ -240,8 +242,7 @@ class FRadioStyle with Diagnosticable { ..add(DiagnosticsProperty('labelLayoutStyle', labelLayoutStyle)) ..add(DiagnosticsProperty('enabledStyle', enabledStyle)) ..add(DiagnosticsProperty('disabledStyle', disabledStyle)) - ..add(DiagnosticsProperty('errorStyle', errorStyle)) - ..add(DiagnosticsProperty('labelStyle', labelStyle)); + ..add(DiagnosticsProperty('errorStyle', errorStyle)); } @override diff --git a/forui/lib/src/widgets/resizable/resizable.dart b/forui/lib/src/widgets/resizable/resizable.dart index 41b924ec2..7bef23e48 100644 --- a/forui/lib/src/widgets/resizable/resizable.dart +++ b/forui/lib/src/widgets/resizable/resizable.dart @@ -5,7 +5,7 @@ import 'package:meta/meta.dart'; import 'package:sugar/sugar.dart'; import 'package:forui/forui.dart'; -import 'package:forui/src/foundation/platform.dart'; +import 'package:forui/src/foundation/tappable.dart'; import 'package:forui/src/widgets/resizable/divider.dart'; /// A resizable allows its children to be resized along either the horizontal or vertical main axis. @@ -85,7 +85,7 @@ class FResizable extends StatefulWidget { 'The hitRegionExtent should be positive, but is $hitRegionExtent.', ), controller = controller ?? FResizableController.cascade(), - hitRegionExtent = hitRegionExtent ?? (touchPlatforms.contains(defaultTargetPlatform) ? 60 : 10); + hitRegionExtent = hitRegionExtent ?? (Touch.primary ? 60 : 10); @override State createState() => _FResizableState(); diff --git a/forui/lib/src/widgets/select_group/select_group.dart b/forui/lib/src/widgets/select_group/select_group.dart index 55395f7ce..6422c2e3d 100644 --- a/forui/lib/src/widgets/select_group/select_group.dart +++ b/forui/lib/src/widgets/select_group/select_group.dart @@ -267,9 +267,10 @@ class FSelectGroupStyle with Diagnosticable { } /// The [FLabel]'s style. + // ignore: diagnostic_describe_all_properties FLabelStyle get labelStyle => ( layout: labelLayoutStyle, - state: FLabelStateStyle( + state: FLabelStateStyles( enabledStyle: enabledStyle, disabledStyle: disabledStyle, errorStyle: errorStyle, @@ -303,7 +304,6 @@ class FSelectGroupStyle with Diagnosticable { ..add(DiagnosticsProperty('enabledStyle', enabledStyle)) ..add(DiagnosticsProperty('disabledStyle', disabledStyle)) ..add(DiagnosticsProperty('errorStyle', errorStyle)) - ..add(DiagnosticsProperty('labelStyle', labelStyle)) ..add(DiagnosticsProperty('checkboxStyle', checkboxStyle)) ..add(DiagnosticsProperty('radioStyle', radioStyle)); } diff --git a/forui/lib/src/widgets/slider/form_field.dart b/forui/lib/src/widgets/slider/form_field.dart new file mode 100644 index 000000000..63ee9ae7a --- /dev/null +++ b/forui/lib/src/widgets/slider/form_field.dart @@ -0,0 +1,160 @@ +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/slider/inherited_data.dart'; +import 'package:forui/src/widgets/slider/inherited_state.dart'; +import 'package:forui/src/widgets/slider/slider_render_object.dart'; +import 'package:forui/src/widgets/slider/track.dart'; + +@internal +class SliderFormField extends FormField { + final FSliderController controller; + final BoxConstraints constraints; + final Widget? label; + final Widget? description; + final Widget Function(BuildContext, String) errorBuilder; + + SliderFormField({ + required this.controller, + required this.constraints, + required this.label, + required this.description, + required this.errorBuilder, + super.onSaved, + super.validator, + super.forceErrorText, + super.enabled = true, + super.autovalidateMode, + super.restorationId, + super.key, + }) : super( + builder: (field) { + final state = field as _State; + final InheritedData(:layout, :marks, :trackMainAxisExtent) = InheritedData.of(state.context); + final style = InheritedData.of(state.context).style; + final (labelState, stateStyle) = switch ((state.hasError, enabled)) { + (false, true) => (FLabelState.enabled, style.enabledStyle), + (false, false) => (FLabelState.disabled, style.disabledStyle), + (true, _) => (FLabelState.error, style.errorStyle), + }; + + // DO NOT REORDER THE CHILDREN - _RenderSlider assumes this order. + final children = [ + Padding( + padding: style.labelLayoutStyle.childPadding, + child: const Track(), + ), + if (label != null) + DefaultTextStyle( + style: stateStyle.labelTextStyle, + child: Padding( + padding: style.labelLayoutStyle.labelPadding, + child: label, + ), + ) + else + const SizedBox(), + if (description != null) + DefaultTextStyle.merge( + style: stateStyle.descriptionTextStyle, + child: Padding( + padding: style.labelLayoutStyle.descriptionPadding, + child: description, + ), + ) + else + const SizedBox(), + if (state.errorText != null) + DefaultTextStyle.merge( + style: (stateStyle as FSliderErrorStyle).errorTextStyle, + child: Padding( + padding: style.labelLayoutStyle.errorPadding, + child: errorBuilder(state.context, state.errorText!), + ), + ) + else + const SizedBox(), + for (final mark in marks.where((mark) => mark.label != null).toList()) + if (mark case FSliderMark(:final style, :final label?)) + DefaultTextStyle( + style: (style ?? stateStyle.markStyle).labelTextStyle, + child: label, + ), + ]; + + return InheritedState( + style: stateStyle, + state: labelState, + child: layout.vertical + ? VerticalSliderRenderObject(children: children) + : HorizontalSliderRenderObject(children: children), + ); + }, + ); + + @override + FormFieldState createState() => _State(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('controller', controller)) + ..add(DiagnosticsProperty('constraints', constraints)) + ..add(ObjectFlagProperty.has('errorBuilder', errorBuilder)); + } +} + +class _State extends FormFieldState { + @override + void initState() { + super.initState(); + widget.controller.addListener(_handleControllerChanged); + } + + @override + void didUpdateWidget(covariant SliderFormField old) { + super.didUpdateWidget(old); + if (widget.controller == old.controller) { + return; + } + + widget.controller.addListener(_handleControllerChanged); + old.controller.removeListener(_handleControllerChanged); + } + + @override + void didChange(FSliderSelection? value) { + // This is not 100% accurate since a controller's selection can never be null. However, users will have to go out + // of their way to obtain a FormFieldState via a GlobalKey AND call didChange(null). + assert(value != null, "A slider's selection cannot be null."); + super.didChange(value); + if (widget.controller.selection != value) { + widget.controller.selection = value; + } + } + + @override + void reset() { + // Set the controller value before calling super.reset() to let _handleControllerChanged suppress the change. + widget.controller.reset(); + super.reset(); + } + + void _handleControllerChanged() { + // Suppress changes that originated from within this class. + // + // In the case where a controller has been passed in to this widget, we register this change listener. In these + // cases, we'll also receive change notifications for changes originating from within this class -- for example, the + // reset() method. In such cases, the FormField value will already have been set. + if (widget.controller.selection != value) { + didChange(widget.controller.selection); + } + } + + @override + SliderFormField get widget => super.widget as SliderFormField; +} diff --git a/forui/lib/src/widgets/slider/inherited_data.dart b/forui/lib/src/widgets/slider/inherited_data.dart index 9ff5db0af..533525716 100644 --- a/forui/lib/src/widgets/slider/inherited_data.dart +++ b/forui/lib/src/widgets/slider/inherited_data.dart @@ -17,6 +17,8 @@ final class InheritedData extends InheritedWidget { final FSliderStyle style; final Layout layout; final List marks; + final double? trackMainAxisExtent; + final double? trackHitRegionCrossExtent; final Widget Function(FTooltipStyle, double) tooltipBuilder; final String Function(FSliderSelection) semanticFormatterCallback; final String Function(double) semanticValueFormatterCallback; @@ -26,6 +28,8 @@ final class InheritedData extends InheritedWidget { required this.style, required this.layout, required this.marks, + required this.trackMainAxisExtent, + required this.trackHitRegionCrossExtent, required this.tooltipBuilder, required this.semanticFormatterCallback, required this.semanticValueFormatterCallback, @@ -39,6 +43,8 @@ final class InheritedData extends InheritedWidget { style != old.style || layout != old.layout || !marks.equals(marks) || + trackMainAxisExtent != old.trackMainAxisExtent || + trackHitRegionCrossExtent != old.trackHitRegionCrossExtent || tooltipBuilder != old.tooltipBuilder || semanticFormatterCallback != old.semanticFormatterCallback || semanticValueFormatterCallback != old.semanticValueFormatterCallback || @@ -51,6 +57,8 @@ final class InheritedData extends InheritedWidget { ..add(DiagnosticsProperty('style', style)) ..add(EnumProperty('layout', layout)) ..add(IterableProperty('marks', marks)) + ..add(DoubleProperty('trackMainAxisExtent', trackMainAxisExtent)) + ..add(DoubleProperty('trackHitRegionCrossExtent', trackHitRegionCrossExtent)) ..add(ObjectFlagProperty.has('tooltipBuilder', tooltipBuilder)) ..add(ObjectFlagProperty.has('semanticFormatterCallback', semanticFormatterCallback)) ..add(ObjectFlagProperty.has('semanticFormatterCallback', semanticValueFormatterCallback)) diff --git a/forui/lib/src/widgets/slider/inherited_state.dart b/forui/lib/src/widgets/slider/inherited_state.dart new file mode 100644 index 000000000..ddc83e264 --- /dev/null +++ b/forui/lib/src/widgets/slider/inherited_state.dart @@ -0,0 +1,36 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; + +import 'package:meta/meta.dart'; + +import 'package:forui/forui.dart'; + +@internal +class InheritedState extends InheritedWidget { + static InheritedState of(BuildContext context) { + final InheritedState? result = context.dependOnInheritedWidgetOfExactType(); + assert(result != null, 'No InheritedState found in context'); + return result!; + } + + final FSliderStateStyle style; + final FLabelState state; + + const InheritedState({ + required this.style, + required this.state, + required super.child, + super.key, + }); + + @override + bool updateShouldNotify(InheritedState old) => style != old.style || state != old.state; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('style', style)) + ..add(DiagnosticsProperty('state', state)); + } +} diff --git a/forui/lib/src/widgets/slider/layout.dart b/forui/lib/src/widgets/slider/layout.dart deleted file mode 100644 index 3cdcee16f..000000000 --- a/forui/lib/src/widgets/slider/layout.dart +++ /dev/null @@ -1,273 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter/widgets.dart'; - -import 'package:meta/meta.dart'; -import 'package:sugar/sugar.dart' hide Offset; - -import 'package:forui/forui.dart'; -import 'package:forui/src/foundation/alignment.dart'; -import 'package:forui/src/widgets/slider/inherited_controller.dart'; -import 'package:forui/src/widgets/slider/inherited_data.dart'; -import 'package:forui/src/widgets/slider/track.dart'; - -@internal -class SliderLayout extends StatefulWidget { - final FSliderController controller; - final FSliderStyle style; - final Layout layout; - final List marks; - final BoxConstraints constraints; - - const SliderLayout({ - required this.controller, - required this.style, - required this.layout, - required this.marks, - required this.constraints, - super.key, - }); - - @override - State createState() => _SliderLayoutState(); - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - ..add(DiagnosticsProperty('controller', controller)) - ..add(DiagnosticsProperty('style', style)) - ..add(EnumProperty('layout', layout)) - ..add(IterableProperty('marks', marks)) - ..add(DiagnosticsProperty('constraints', constraints)); - } -} - -class _SliderLayoutState extends State { - @override - void initState() { - super.initState(); - final mainAxisExtent = widget.layout.vertical ? widget.constraints.maxHeight : widget.constraints.maxWidth; - widget.controller.attach(mainAxisExtent - widget.style.thumbStyle.size, widget.marks); - } - - @override - void didUpdateWidget(covariant SliderLayout old) { - super.didUpdateWidget(old); - final mainAxisExtent = (widget.layout.vertical ? widget.constraints.maxHeight : widget.constraints.maxWidth) - - widget.style.thumbStyle.size; - final oldMainAxisExtent = - (old.layout.vertical ? old.constraints.maxHeight : old.constraints.maxWidth) - old.style.thumbStyle.size; - - if (widget.controller != old.controller || - widget.layout != old.layout || - mainAxisExtent != oldMainAxisExtent || - !widget.marks.equals(old.marks)) { - widget.controller.attach(mainAxisExtent, widget.marks); - } - } - - @override - Widget build(BuildContext context) { - final InheritedData(:style, :semanticFormatterCallback, :enabled) = InheritedData.of(context); - final markStyle = style.markStyle; - final marks = widget.marks.where((mark) => mark.label != null).toList(); - - return ListenableBuilder( - listenable: widget.controller, - builder: (_, child) => InheritedController( - controller: widget.controller, - child: child!, - ), - child: _Slider( - style: widget.style, - layout: widget.layout, - marks: marks, - children: [ - const Track(), - for (final mark in marks) - if (mark case FSliderMark(:final style, :final label?)) - DefaultTextStyle( - style: (style ?? markStyle).labelTextStyle, - child: label, - ), - ], - ), - ); - } -} - -class _Slider extends MultiChildRenderObjectWidget { - final FSliderStyle style; - final Layout layout; - final List marks; - - const _Slider({ - required this.style, - required this.layout, - required this.marks, - required super.children, - }); - - @override - RenderObject createRenderObject(BuildContext context) => _RenderSlider(style, layout, marks); - - @override - void updateRenderObject(BuildContext context, covariant _RenderSlider renderObject) { - renderObject - ..style = style - ..sliderLayout = layout - ..marks = marks; - } - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - ..add(DiagnosticsProperty('style', style)) - ..add(EnumProperty('layout', layout)) - ..add(IterableProperty('marks', marks)); - } -} - -class _Data extends ContainerBoxParentData with ContainerParentDataMixin {} - -class _RenderSlider extends RenderBox - with ContainerRenderObjectMixin, RenderBoxContainerDefaultsMixin { - FSliderStyle _style; - Layout _layout; - List _marks; - - _RenderSlider(this._style, this._layout, this._marks); - - @override - void setupParentData(covariant RenderObject child) { - child.parentData = _Data(); - } - - @override - void performLayout() { - final loosened = constraints.loosen(); - final Rect Function(RenderBox, Size, FSliderMark, FSliderMarkStyle) position = switch (_layout) { - Layout.ltr => (track, label, mark, style) { - final offset = _anchor(track.size.width, mark.value, style); - return _rect(label, mark, Offset(offset.$1, offset.$2), style); - }, - Layout.rtl => (track, size, mark, style) { - final offset = _anchor(track.size.width, 1 - mark.value, style); - return _rect(size, mark, Offset(offset.$1, offset.$2), style); - }, - Layout.ttb => (track, size, mark, style) { - final offset = _anchor(track.size.height, mark.value, style); - return _rect(size, mark, Offset(offset.$2, offset.$1), style); - }, - Layout.btt => (track, size, mark, style) { - final offset = _anchor(track.size.height, 1 - mark.value, style); - return _rect(size, mark, Offset(offset.$2, offset.$1), style); - }, - }; - - final track = firstChild!..layout(loosened, parentUsesSize: true); - - final labels = {}; - var label = childAfter(track); - var minX = 0.0; - var minY = 0.0; - var maxX = track.size.width; - var maxY = track.size.height; - - for (final mark in _marks) { - if (label == null) { - break; - } - - label.layout(loosened, parentUsesSize: true); - - final rect = position(track, label.size, mark, mark.style ?? _style.markStyle); - labels[label] = rect; - - minX = min(minX, rect.left); - maxX = max(maxX, rect.right); - minY = min(minY, rect.top); - maxY = max(maxY, rect.bottom); - - label = childAfter(label); - } - - final origin = switch (_layout.vertical) { - true => Offset(labels.values.map((rect) => rect.left).where((x) => x.isNegative).min?.abs() ?? 0, 0), - false => Offset(0, labels.values.map((rect) => rect.top).where((x) => x.isNegative).min?.abs() ?? 0), - }; - - (track.parentData! as _Data).offset = origin; - - for (final MapEntry(key: label, value: rect) in labels.entries) { - (label.parentData! as _Data).offset = rect.topLeft + origin; - } - - size = constraints.constrain(Size(maxX - minX, maxY - minY)); - } - - (double, double) _anchor(double extent, double offset, FSliderMarkStyle markStyle) { - final thumb = _style.thumbStyle.size; - final trackMainAxis = (extent - thumb) * offset; - final anchorMainAxis = (thumb / 2) + trackMainAxis; - final anchorCrossAxis = markStyle.labelOffset + (markStyle.labelOffset < 0 ? 0.0 : _style.crossAxisExtent * 2); - - return (anchorMainAxis, anchorCrossAxis); - } - - Rect _rect(Size size, FSliderMark mark, Offset anchor, FSliderMarkStyle markStyle) { - final rect = anchor & size; - return rect.shift(anchor - markStyle.labelAnchor.relative(to: size, origin: anchor)); - } - - @override - void paint(PaintingContext context, Offset offset) => defaultPaint(context, offset); - - @override - bool hitTestChildren(BoxHitTestResult result, {required Offset position}) => - defaultHitTestChildren(result, position: position); - - FSliderStyle get style => _style; - - set style(FSliderStyle value) { - if (_style == value) { - return; - } - - _style = value; - markNeedsLayout(); - } - - Layout get sliderLayout => _layout; - - set sliderLayout(Layout value) { - if (_layout == value) { - return; - } - - _layout = value; - markNeedsLayout(); - } - - List get marks => _marks; - - set marks(List value) { - if (_marks.equals(value)) { - return; - } - - _marks = value; - markNeedsLayout(); - } - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - ..add(DiagnosticsProperty('style', style)) - ..add(EnumProperty('layout', sliderLayout)) - ..add(IterableProperty('marks', marks)); - } -} diff --git a/forui/lib/src/widgets/slider/slider.dart b/forui/lib/src/widgets/slider/slider.dart index 7fc82356d..b1d0c47e3 100644 --- a/forui/lib/src/widgets/slider/slider.dart +++ b/forui/lib/src/widgets/slider/slider.dart @@ -1,11 +1,12 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; -import 'package:meta/meta.dart'; +import 'package:collection/collection.dart'; import 'package:forui/forui.dart'; +import 'package:forui/src/widgets/slider/form_field.dart'; +import 'package:forui/src/widgets/slider/inherited_controller.dart'; import 'package:forui/src/widgets/slider/inherited_data.dart'; -import 'package:forui/src/widgets/slider/layout.dart'; /// An input where the user selects a value from within a given range. /// @@ -18,7 +19,9 @@ import 'package:forui/src/widgets/slider/layout.dart'; /// * [FDiscreteSliderController.new] for selecting a discrete value. /// * [FDiscreteSliderController.range] for selecting a discrete range. /// * [FSliderStyles] for customizing a slider's appearance. -class FSlider extends FormField { +class FSlider extends StatelessWidget { + static Widget _errorBuilder(BuildContext context, String error) => Text(error); + static Widget _tooltipBuilder(FTooltipStyle _, double value) => Text('${(value * 100).toStringAsFixed(0)}%'); static String Function(FSliderSelection) _formatter(FSliderController controller) => switch (controller.extendable) { @@ -39,9 +42,35 @@ class FSlider extends FormField { /// The layout. Defaults to [Layout.ltr]. final Layout layout; + /// The label. + final Widget? label; + + /// The description. + final Widget? description; + + /// The builder for errors displayed below the [description]. Defaults to displaying the error message. + final Widget Function(BuildContext, String) errorBuilder; + /// The marks. final List marks; + /// The extent of the track along the main axis. Defaults to occupying the maximum amount of space possible along the + /// main axis. + /// + /// **Contract**: + /// Throws [AssertionError] if [trackMainAxisExtent] is not positive. + /// + /// Throws [StateError] if [trackMainAxisExtent], either [label], [description], or [forceErrorText] is given, + /// and [layout] is vertical. + final double? trackMainAxisExtent; + + /// The extent of the track's hit region in the cross-axis direction. + /// + /// Defaults to: + /// * either the tracker the thumb's cross extent, whichever is larger, on primarily touch devices. + /// * 0 on non-primarily touch devices. + final double? trackHitRegionCrossExtent; + /// A builder that creates the tooltip. Defaults to printing the current percentage. final Widget Function(FTooltipStyle, double) tooltipBuilder; @@ -55,61 +84,109 @@ class FSlider extends FormField { // ignore: avoid_positional_boolean_parameters final String Function(double) semanticValueFormatterCallback; + /// An optional method to call with the final value when the form is saved via [FormState.save]. + final FormFieldSetter? onSaved; + + /// An optional method that validates an input. Returns an error string to + /// display if the input is invalid, or null otherwise. + /// + /// The returned value is exposed by the [FormFieldState.errorText] property. It transforms the text using + /// [errorBuilder]. + /// + /// Alternating between error and normal state can cause the height of the [FTextField] to change if no other + /// subtext decoration is set on the field. To create a field whose height is fixed regardless of whether or not an + /// error is displayed, wrap the [FTextField] in a fixed height parent like [SizedBox]. + final FormFieldValidator? validator; + + /// Used to enable/disable this form field auto validation and update its error text. + /// + /// Defaults to [AutovalidateMode.disabled]. + /// + /// If [AutovalidateMode.onUserInteraction], this FormField will only auto-validate after its content changes. If + /// [AutovalidateMode.always], it will auto-validate even without user interaction. If [AutovalidateMode.disabled], + /// auto-validation will be disabled. + final AutovalidateMode? autovalidateMode; + + /// An optional property that forces the [FormFieldState] into an error state + /// by directly setting the [FormFieldState.errorText] property without + /// running the validator function. + /// + /// When the [forceErrorText] property is provided, the [FormFieldState.errorText] + /// will be set to the provided value, causing the form field to be considered + /// invalid and to display the error message specified. + /// + /// When [validator] is provided, [forceErrorText] will override any error that it + /// returns. [validator] will not be called unless [forceErrorText] is null. + final String? forceErrorText; + + /// True if the slider is enabled. + final bool enabled; + /// Creates a [FSlider]. FSlider({ required this.controller, this.style, this.layout = Layout.ltr, + this.label, + this.description, + this.errorBuilder = _errorBuilder, this.marks = const [], + this.trackMainAxisExtent, + this.trackHitRegionCrossExtent, this.tooltipBuilder = _tooltipBuilder, this.semanticValueFormatterCallback = _semanticValueFormatter, String Function(FSliderSelection)? semanticFormatterCallback, - super.onSaved, - super.validator, - super.forceErrorText, - super.enabled = true, - super.autovalidateMode, - super.restorationId, + this.onSaved, + this.validator, + this.forceErrorText, + this.enabled = true, + this.autovalidateMode, super.key, - }) : semanticFormatterCallback = semanticFormatterCallback ?? _formatter(controller), - super( - builder: (field) { - final state = field as _State; - final styles = state.context.theme.sliderStyles; - final sliderStyle = style ?? - switch ((state.hasError, enabled, layout.vertical)) { - (true, _, false) => styles.errorHorizontalStyle, - (true, _, true) => styles.errorVerticalStyle, - (false, true, false) => styles.enabledHorizontalStyle, - (false, true, true) => styles.enabledVerticalStyle, - (false, false, false) => styles.disabledHorizontalStyle, - (false, false, true) => styles.disabledVerticalStyle, - }; - - // TODO wrap in FLabel. - return InheritedData( - style: sliderStyle, - layout: layout, - marks: marks, - enabled: enabled, - tooltipBuilder: tooltipBuilder, - semanticFormatterCallback: semanticFormatterCallback ?? _formatter(controller), - semanticValueFormatterCallback: semanticValueFormatterCallback, - child: LayoutBuilder( - builder: (context, constraints) => SliderLayout( - controller: controller, - style: sliderStyle, - layout: layout, - marks: marks, - constraints: constraints, - ), - ), - ); - }, - ); + }) : semanticFormatterCallback = semanticFormatterCallback ?? _formatter(controller) { + if (trackMainAxisExtent == null && + (label != null || description != null || forceErrorText != null) && + layout.vertical) { + throw StateError( + 'A vertical FSlider was given a label, description, or forceErrorText although it needs a trackMainAxisExtent. ' + 'To fix this, consider supplying a trackMainAxisExtent or changing the layout to horizontal.', + ); + } + } @override - FormFieldState createState() => _State(); + Widget build(BuildContext context) { + final styles = context.theme.sliderStyles; + final sliderStyle = style ?? (layout.vertical ? styles.verticalStyle : styles.horizontalStyle); + return InheritedData( + style: sliderStyle, + layout: layout, + marks: marks, + trackMainAxisExtent: trackMainAxisExtent, + trackHitRegionCrossExtent: trackHitRegionCrossExtent, + enabled: enabled, + tooltipBuilder: tooltipBuilder, + semanticFormatterCallback: semanticFormatterCallback, + semanticValueFormatterCallback: semanticValueFormatterCallback, + child: LayoutBuilder( + builder: (context, constraints) => _Slider( + controller: controller, + style: sliderStyle, + layout: layout, + label: label, + description: description, + errorBuilder: errorBuilder, + marks: marks, + constraints: constraints, + mainAxisExtent: trackMainAxisExtent, + onSaved: onSaved, + validator: validator, + autovalidateMode: autovalidateMode, + forceErrorText: forceErrorText, + enabled: enabled, + ), + ), + ); + } @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { @@ -118,343 +195,133 @@ class FSlider extends FormField { ..add(DiagnosticsProperty('controller', controller)) ..add(DiagnosticsProperty('style', style)) ..add(EnumProperty('layout', layout)) + ..add(ObjectFlagProperty.has('errorBuilder', errorBuilder)) ..add(IterableProperty('marks', marks)) - ..add(FlagProperty('enabled', value: enabled, ifTrue: 'enabled', ifFalse: 'disabled')) + ..add(DoubleProperty('trackMainAxisExtent', trackMainAxisExtent)) + ..add(DoubleProperty('trackHitRegionCrossExtent', trackHitRegionCrossExtent)) ..add(ObjectFlagProperty.has('tooltipBuilder', tooltipBuilder)) ..add(ObjectFlagProperty.has('semanticFormatterCallback', semanticFormatterCallback)) - ..add(ObjectFlagProperty.has('semanticValueFormatterCallback', semanticValueFormatterCallback)); + ..add(ObjectFlagProperty.has('semanticValueFormatterCallback', semanticValueFormatterCallback)) + ..add(ObjectFlagProperty.has('onSaved', onSaved)) + ..add(ObjectFlagProperty.has('validator', validator)) + ..add(FlagProperty('enabled', value: enabled, ifTrue: 'enabled', ifFalse: 'disabled')) + ..add(EnumProperty('autovalidateMode', autovalidateMode)) + ..add(ObjectFlagProperty.has('forceErrorText', forceErrorText)); } } -class _State extends FormFieldState { - @override - void initState() { - super.initState(); - widget.controller.addListener(_handleControllerChanged); - } - - @override - void didUpdateWidget(covariant FSlider old) { - super.didUpdateWidget(old); - if (widget.controller == old.controller) { - return; - } - - widget.controller.addListener(_handleControllerChanged); - old.controller.removeListener(_handleControllerChanged); - } - - @override - void didChange(FSliderSelection? value) { - super.didChange(value); - // This is not 100% accurate since a controller's selection can never be null. However, users will have to go out - // of their way to obtain a FormFieldState via a GlobalKey AND call didChange(null). - assert(value != null, "A slider's selection cannot be null."); - if (widget.controller.selection != value) { - widget.controller.selection = value; - } - } +class _Slider extends StatefulWidget { + final FSliderController controller; + final FSliderStyle style; + final Layout layout; + final Widget? label; + final Widget? description; + final Widget Function(BuildContext, String) errorBuilder; + final List marks; + final BoxConstraints constraints; + final double? mainAxisExtent; + final FormFieldSetter? onSaved; + final FormFieldValidator? validator; + final AutovalidateMode? autovalidateMode; + final String? forceErrorText; + final bool enabled; + + const _Slider({ + required this.controller, + required this.style, + required this.layout, + required this.label, + required this.description, + required this.errorBuilder, + required this.marks, + required this.constraints, + required this.mainAxisExtent, + required this.onSaved, + required this.validator, + required this.autovalidateMode, + required this.forceErrorText, + required this.enabled, + }); @override - void reset() { - // Set the controller value before calling super.reset() to let _handleControllerChanged suppress the change. - widget.controller.reset(); - super.reset(); - } - - void _handleControllerChanged() { - // Suppress changes that originated from within this class. - // - // In the case where a controller has been passed in to this widget, we register this change listener. In these - // cases, we'll also receive change notifications for changes originating from within this class -- for example, the - // reset() method. In such cases, the FormField value will already have been set. - if (widget.controller.selection != value) { - didChange(widget.controller.selection); + State<_Slider> createState() => _SliderState(); + + double get _mainAxisExtent { + final insets = style.labelLayoutStyle.childPadding; + final extent = switch (mainAxisExtent) { + final extent? => extent, + _ when layout.vertical => constraints.maxHeight - insets.top - insets.bottom, + _ => constraints.maxWidth - insets.left - insets.right, + }; + + if (extent.isInfinite) { + throw FlutterError( + switch (layout.vertical) { + true => 'A vertical FSlider was given an infinite height although it needs a finite height. To fix this, ' + 'consider supplying a mainAxisExtent or placing FSlider in a SizedBox.', + false => 'A horizontal FSlider was given an infinite width although it needs a finite width. To fix this, ' + 'consider supplying a mainAxisExtent or placing FSlider in a SizedBox.', + }, + ); } - } - - @override - FSlider get widget => super.widget as FSlider; -} -/// A slider's styles. -final class FSliderStyles with Diagnosticable { - /// The enabled slider's horizontal style. - final FSliderStyle enabledHorizontalStyle; - - /// The enabled slider's vertical style. - final FSliderStyle enabledVerticalStyle; - - /// The disabled slider's horizontal style. - final FSliderStyle disabledHorizontalStyle; - - /// The disabled slider's vertical style. - final FSliderStyle disabledVerticalStyle; - - /// The error slider's horizontal style. - final FSliderStyle errorHorizontalStyle; - - /// The error slider's vertical style. - final FSliderStyle errorVerticalStyle; - - /// Creates a [FSliderStyles]. - FSliderStyles({ - required this.enabledHorizontalStyle, - required this.enabledVerticalStyle, - required this.disabledHorizontalStyle, - required this.disabledVerticalStyle, - required this.errorHorizontalStyle, - required this.errorVerticalStyle, - }); - - /// Creates a [FSliderStyles] that inherits its properties from the given [FColorScheme]. - factory FSliderStyles.inherit({ - required FColorScheme colorScheme, - required FTypography typography, - required FStyle style, - }) { - final enabledHorizontalStyle = FSliderStyle( - activeColor: colorScheme.primary, - inactiveColor: colorScheme.secondary, - markStyle: FSliderMarkStyle( - tickColor: colorScheme.mutedForeground, - labelTextStyle: typography.xs.copyWith(color: colorScheme.primary), - labelAnchor: Alignment.topCenter, - labelOffset: 7.5, - ), - tooltipStyle: FTooltipStyle.inherit(colorScheme: colorScheme, typography: typography, style: style), - thumbStyle: FSliderThumbStyle( - color: colorScheme.primaryForeground, - borderColor: colorScheme.primary, - ), - ); - - final disabledHorizontalStyle = FSliderStyle( - activeColor: colorScheme.primary.withOpacity(0.7), - inactiveColor: colorScheme.secondary, - markStyle: FSliderMarkStyle( - tickColor: colorScheme.mutedForeground, - labelTextStyle: typography.xs.copyWith(color: colorScheme.primary.withOpacity(0.7)), - labelAnchor: Alignment.topCenter, - labelOffset: 7.5, - ), - tooltipStyle: FTooltipStyle.inherit(colorScheme: colorScheme, typography: typography, style: style), - thumbStyle: FSliderThumbStyle( - color: colorScheme.primaryForeground, - borderColor: colorScheme.primary.withOpacity(0.7), - ), - ); - - final errorHorizontalStyle = FSliderStyle( - activeColor: colorScheme.error, - inactiveColor: colorScheme.secondary, - markStyle: FSliderMarkStyle( - tickColor: colorScheme.mutedForeground, - labelTextStyle: typography.xs.copyWith(color: colorScheme.error), - labelAnchor: Alignment.topCenter, - labelOffset: 7.5, - ), - tooltipStyle: FTooltipStyle.inherit(colorScheme: colorScheme, typography: typography, style: style), - thumbStyle: FSliderThumbStyle( - color: colorScheme.errorForeground, - borderColor: colorScheme.error, - ), - ); - - return FSliderStyles( - enabledHorizontalStyle: enabledHorizontalStyle, - enabledVerticalStyle: enabledHorizontalStyle.copyWith( - markStyle: FSliderMarkStyle( - tickColor: colorScheme.mutedForeground, - labelTextStyle: typography.xs.copyWith(color: colorScheme.primary), - labelAnchor: Alignment.centerRight, - labelOffset: -7.5, - ), - ), - disabledHorizontalStyle: disabledHorizontalStyle, - disabledVerticalStyle: disabledHorizontalStyle.copyWith( - markStyle: FSliderMarkStyle( - tickColor: colorScheme.mutedForeground, - labelTextStyle: typography.xs.copyWith(color: colorScheme.primary.withOpacity(0.7)), - labelAnchor: Alignment.centerRight, - labelOffset: -7.5, - ), - ), - errorHorizontalStyle: errorHorizontalStyle, - errorVerticalStyle: errorHorizontalStyle.copyWith( - markStyle: FSliderMarkStyle( - tickColor: colorScheme.mutedForeground, - labelTextStyle: typography.xs.copyWith(color: colorScheme.primary), - labelAnchor: Alignment.centerRight, - labelOffset: -7.5, - ), - ), - ); + return extent - style.thumbSize; } - /// Returns a copy of this [FSliderStyles] but with the given fields replaced with the new values. - @useResult - FSliderStyles copyWith({ - FSliderStyle? enabledHorizontalStyle, - FSliderStyle? enabledVerticalStyle, - FSliderStyle? disabledHorizontalStyle, - FSliderStyle? disabledVerticalStyle, - FSliderStyle? errorHorizontalStyle, - FSliderStyle? errorVerticalStyle, - }) => - FSliderStyles( - enabledHorizontalStyle: enabledHorizontalStyle ?? this.enabledHorizontalStyle, - enabledVerticalStyle: enabledVerticalStyle ?? this.enabledVerticalStyle, - disabledHorizontalStyle: disabledHorizontalStyle ?? this.disabledHorizontalStyle, - disabledVerticalStyle: disabledVerticalStyle ?? this.disabledVerticalStyle, - errorHorizontalStyle: errorHorizontalStyle ?? this.errorHorizontalStyle, - errorVerticalStyle: errorVerticalStyle ?? this.errorVerticalStyle, - ); - @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties - ..add(DiagnosticsProperty('enabledHorizontalStyle', enabledHorizontalStyle)) - ..add(DiagnosticsProperty('enabledVerticalStyle', enabledVerticalStyle)) - ..add(DiagnosticsProperty('disabledHorizontalStyle', disabledHorizontalStyle)) - ..add(DiagnosticsProperty('disabledVerticalStyle', disabledVerticalStyle)) - ..add(DiagnosticsProperty('errorHorizontalStyle', errorHorizontalStyle)) - ..add(DiagnosticsProperty('errorVerticalStyle', errorVerticalStyle)); + ..add(DiagnosticsProperty('controller', controller)) + ..add(DiagnosticsProperty('style', style)) + ..add(EnumProperty('layout', layout)) + ..add(ObjectFlagProperty.has('errorBuilder', errorBuilder)) + ..add(IterableProperty('marks', marks)) + ..add(DiagnosticsProperty('constraints', constraints)) + ..add(DoubleProperty('mainAxisExtent', mainAxisExtent)) + ..add(ObjectFlagProperty.has('onSaved', onSaved)) + ..add(ObjectFlagProperty.has('validator', validator)) + ..add(EnumProperty('autovalidateMode', autovalidateMode)) + ..add(ObjectFlagProperty.has('forceErrorText', forceErrorText)) + ..add(FlagProperty('enabled', value: enabled, ifTrue: 'enabled', ifFalse: 'disabled')); } - - @override - bool operator ==(Object other) => - identical(this, other) || - other is FSliderStyles && - runtimeType == other.runtimeType && - enabledHorizontalStyle == other.enabledHorizontalStyle && - enabledVerticalStyle == other.enabledVerticalStyle && - disabledHorizontalStyle == other.disabledHorizontalStyle && - disabledVerticalStyle == other.disabledVerticalStyle && - errorHorizontalStyle == other.errorHorizontalStyle && - errorVerticalStyle == other.errorVerticalStyle; - - @override - int get hashCode => - enabledHorizontalStyle.hashCode ^ - enabledVerticalStyle.hashCode ^ - disabledHorizontalStyle.hashCode ^ - disabledVerticalStyle.hashCode ^ - errorHorizontalStyle.hashCode ^ - errorVerticalStyle.hashCode; } -/// A slider's style. -final class FSliderStyle with Diagnosticable { - /// The slider's active color. - final Color activeColor; - - /// The slider inactive color. - final Color inactiveColor; - - /// The slider's cross-axis extent. Defaults to 8. - /// - /// ## Contract: - /// Throws [AssertionError] if it is not positive. - final double crossAxisExtent; - - /// The slider's border radius. - final BorderRadius borderRadius; - - /// The slider marks' style. - final FSliderMarkStyle markStyle; - - /// The slider thumb's style. - final FSliderThumbStyle thumbStyle; - - /// The tooltip's style. - final FTooltipStyle tooltipStyle; - - //// The anchor of the tooltip to which the [tooltipThumbAnchor] is aligned to. Defaults to [Alignment.bottomCenter]. - final Alignment tooltipTipAnchor; - - /// The anchor of the thumb to which the [tooltipTipAnchor] is aligned to. Defaults to [Alignment.topCenter]. - final Alignment tooltipThumbAnchor; - - /// Creates a [FSliderStyle]. - FSliderStyle({ - required this.activeColor, - required this.inactiveColor, - required this.markStyle, - required this.thumbStyle, - required this.tooltipStyle, - this.tooltipTipAnchor = Alignment.bottomCenter, - this.tooltipThumbAnchor = Alignment.topCenter, - this.crossAxisExtent = 8, - this.borderRadius = const BorderRadius.all(Radius.circular(4)), - }); - - /// Returns a copy of this [FSliderStyle] but with the given fields replaced with the new values. - @useResult - FSliderStyle copyWith({ - Color? activeColor, - Color? inactiveColor, - double? mainAxisPadding, - double? crossAxisExtent, - BorderRadius? borderRadius, - FSliderMarkStyle? markStyle, - FSliderThumbStyle? thumbStyle, - FTooltipStyle? tooltipStyle, - Alignment? tooltipTipAnchor, - Alignment? tooltipThumbAnchor, - }) => - FSliderStyle( - activeColor: activeColor ?? this.activeColor, - inactiveColor: inactiveColor ?? this.inactiveColor, - crossAxisExtent: crossAxisExtent ?? this.crossAxisExtent, - borderRadius: borderRadius ?? this.borderRadius, - markStyle: markStyle ?? this.markStyle, - thumbStyle: thumbStyle ?? this.thumbStyle, - tooltipStyle: tooltipStyle ?? this.tooltipStyle, - tooltipTipAnchor: tooltipTipAnchor ?? this.tooltipTipAnchor, - tooltipThumbAnchor: tooltipThumbAnchor ?? this.tooltipThumbAnchor, - ); - +class _SliderState extends State<_Slider> { @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - ..add(ColorProperty('activeColor', activeColor)) - ..add(ColorProperty('inactiveColor', inactiveColor)) - ..add(DoubleProperty('crossAxisExtent', crossAxisExtent)) - ..add(DiagnosticsProperty('borderRadius', borderRadius)) - ..add(DiagnosticsProperty('markStyle', markStyle)) - ..add(DiagnosticsProperty('thumbStyle', thumbStyle)) - ..add(DiagnosticsProperty('tooltipStyle', tooltipStyle)) - ..add(DiagnosticsProperty('tooltipTipAnchor', tooltipTipAnchor)) - ..add(DiagnosticsProperty('tooltipThumbAnchor', tooltipThumbAnchor)); + void initState() { + super.initState(); + widget.controller.attach(widget._mainAxisExtent, widget.marks); } @override - bool operator ==(Object other) => - identical(this, other) || - other is FSliderStyle && - runtimeType == other.runtimeType && - activeColor == other.activeColor && - inactiveColor == other.inactiveColor && - crossAxisExtent == other.crossAxisExtent && - borderRadius == other.borderRadius && - markStyle == other.markStyle && - thumbStyle == other.thumbStyle && - tooltipStyle == other.tooltipStyle && - tooltipTipAnchor == other.tooltipTipAnchor && - tooltipThumbAnchor == other.tooltipThumbAnchor; + void didUpdateWidget(covariant _Slider old) { + super.didUpdateWidget(old); + if (widget.controller != old.controller || + widget.layout != old.layout || + widget._mainAxisExtent != old._mainAxisExtent || + !widget.marks.equals(old.marks)) { + widget.controller.attach(widget._mainAxisExtent, widget.marks); + } + } @override - int get hashCode => - activeColor.hashCode ^ - inactiveColor.hashCode ^ - crossAxisExtent.hashCode ^ - borderRadius.hashCode ^ - markStyle.hashCode ^ - thumbStyle.hashCode ^ - tooltipStyle.hashCode ^ - tooltipTipAnchor.hashCode ^ - tooltipThumbAnchor.hashCode; + Widget build(BuildContext context) => ListenableBuilder( + listenable: widget.controller, + builder: (context, _) => InheritedController( + controller: widget.controller, + child: SliderFormField( + controller: widget.controller, + constraints: widget.constraints, + label: widget.label, + description: widget.description, + errorBuilder: widget.errorBuilder, + onSaved: widget.onSaved, + validator: widget.validator, + autovalidateMode: widget.autovalidateMode, + forceErrorText: widget.forceErrorText, + enabled: widget.enabled, + ), + ), + ); } diff --git a/forui/lib/src/widgets/slider/slider_controller.dart b/forui/lib/src/widgets/slider/slider_controller.dart index be02bdc72..31943ccfd 100644 --- a/forui/lib/src/widgets/slider/slider_controller.dart +++ b/forui/lib/src/widgets/slider/slider_controller.dart @@ -5,7 +5,6 @@ import 'package:flutter/foundation.dart'; import 'package:meta/meta.dart'; import 'package:forui/forui.dart'; -import 'package:forui/src/foundation/platform.dart'; import 'package:forui/src/widgets/slider/slider_selection.dart'; /// Possible ways for a user to interact with a slider. @@ -31,18 +30,10 @@ enum FSliderInteraction { /// * [FDiscreteSliderController.new] for selecting a discrete value. /// * [FDiscreteSliderController.range] for selecting a discrete range. abstract class FSliderController extends ChangeNotifier { - static FSliderInteraction get _platform => - touchPlatforms.contains(defaultTargetPlatform) ? FSliderInteraction.slide : FSliderInteraction.tapAndSlideThumb; - /// True if the registered tooltip(s) should be shown when the user interacts with the slider. Defaults to true. final FSliderTooltipsController tooltips; - /// The allowed ways to interaction with the slider. - /// - /// Single-value sliders default to [FSliderInteraction.slide] on Android, Fuchsia and iOS, and - /// [FSliderInteraction.tapAndSlideThumb] on other platforms. - /// - /// Range sliders always default to [FSliderInteraction.tapAndSlideThumb]. + /// The allowed ways to interaction with the slider. Defaults to [FSliderInteraction.tapAndSlideThumb]. final FSliderInteraction allowedInteraction; /// Whether the active track is extendable at its min and max edges. @@ -53,8 +44,8 @@ abstract class FSliderController extends ChangeNotifier { /// Creates a [FSliderController] for selecting a single value. FSliderController({ - required this.allowedInteraction, required FSliderSelection selection, + this.allowedInteraction = FSliderInteraction.tapAndSlideThumb, bool tooltips = true, bool minExtendable = false, }) : tooltips = FSliderTooltipsController(enabled: tooltips), @@ -160,10 +151,9 @@ class FContinuousSliderController extends FSliderController { required super.selection, this.stepPercentage = 0.05, super.tooltips = true, - FSliderInteraction? allowedInteraction, + super.allowedInteraction, super.minExtendable, - }) : assert(0 <= stepPercentage && stepPercentage <= 1, 'stepPercentage must be between 0 and 1, inclusive.'), - super(allowedInteraction: allowedInteraction ?? FSliderController._platform); + }) : assert(0 <= stepPercentage && stepPercentage <= 1, 'stepPercentage must be between 0 and 1, inclusive.'); /// Creates a [FContinuousSliderController] for selecting a range. FContinuousSliderController.range({ @@ -208,10 +198,10 @@ class FDiscreteSliderController extends FSliderController { /// Creates a [FDiscreteSliderController] for selecting a single value. FDiscreteSliderController({ required super.selection, - FSliderInteraction? allowedInteraction, + super.allowedInteraction, super.tooltips = true, super.minExtendable, - }) : super(allowedInteraction: allowedInteraction ?? FSliderController._platform); + }); /// Creates a [FDiscreteSliderController] for selecting a range. FDiscreteSliderController.range({ diff --git a/forui/lib/src/widgets/slider/slider_render_object.dart b/forui/lib/src/widgets/slider/slider_render_object.dart new file mode 100644 index 000000000..a88d2c409 --- /dev/null +++ b/forui/lib/src/widgets/slider/slider_render_object.dart @@ -0,0 +1,369 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/rendering.dart'; + +import 'package:collection/collection.dart'; +import 'package:meta/meta.dart'; +import 'package:sugar/sugar.dart' hide Offset; + +import 'package:forui/forui.dart'; +import 'package:forui/src/foundation/rendering.dart'; +import 'package:forui/src/widgets/slider/inherited_data.dart'; +import 'package:forui/src/widgets/slider/inherited_state.dart'; + +@internal +class HorizontalSliderRenderObject extends _SliderRenderObject { + const HorizontalSliderRenderObject({super.children, super.key}); + + @override + RenderObject createRenderObject(BuildContext context) { + final InheritedData(:style, :layout, :marks, :trackMainAxisExtent) = InheritedData.of(context); + final labelledMarks = marks.where((mark) => mark.label != null).toList(); + final stateStyle = InheritedState.of(context).style; + return _RenderHorizontalSlider(style, stateStyle, layout, labelledMarks, trackMainAxisExtent); + } +} + +@internal +class VerticalSliderRenderObject extends _SliderRenderObject { + const VerticalSliderRenderObject({super.children, super.key}); + + @override + RenderObject createRenderObject(BuildContext context) { + final InheritedData(:style, :layout, :marks, :trackMainAxisExtent) = InheritedData.of(context); + final labelledMarks = marks.where((mark) => mark.label != null).toList(); + final stateStyle = InheritedState.of(context).style; + return _RenderVerticalSlider(style, stateStyle, layout, labelledMarks, trackMainAxisExtent); + } +} + +abstract class _SliderRenderObject extends MultiChildRenderObjectWidget { + const _SliderRenderObject({super.key, super.children}); + + @override + // ignore: library_private_types_in_public_api + void updateRenderObject(BuildContext context, covariant _RenderSlider renderObject) { + final InheritedData(:style, :layout, :marks, :trackMainAxisExtent) = InheritedData.of(context); + final stateStyle = InheritedState.of(context).style; + renderObject + ..style = style + ..stateStyle = stateStyle + ..sliderLayout = layout + ..marks = marks.where((mark) => mark.label != null).toList() + ..mainAxisExtent = trackMainAxisExtent; + } +} + +class _RenderHorizontalSlider extends _RenderSlider { + _RenderHorizontalSlider(super._style, super._stateStyle, super._layout, super._marks, super._mainAxisExtent); + + @override + void performLayout() { + final loosened = constraints.loosen(); + + // Layout parts, assuming top-left corner of track/origin is (0, 0). + final insets = _style.labelLayoutStyle.childPadding; + final (:label, :paddedTrack, :description, :error, :slider) = layoutParts( + loosened, + switch (_mainAxisExtent) { + final extent? => loosened.copyWith(maxWidth: extent + insets.left + insets.right), + null => loosened, + }, + ); + + // Calculate offset to move the top/left corner of track/origin from (0, 0), such that no part of the slider has a + // negative offset. + final boxes = [label, paddedTrack, description, error]; + final largest = boxes.order(by: (box) => box.size.width).max!; + boxes.remove(largest); + + // Check whether the marks are larger than the largest label/description/error. + final sliderOffset = Offset( + 0, + label.size.height + (slider.marks.values.map((r) => r.top).where((y) => y.isNegative).minOrNull?.abs() ?? 0), + ); + + paddedTrack.data.offset = sliderOffset; + for (final MapEntry(key: label, value: rect) in slider.marks.entries) { + label.data.offset = rect.topLeft + sliderOffset; + } + + var height = label.size.height + slider.size.height; + description.data.offset = Offset(0, height); + + height += description.size.height; + error.data.offset = Offset(0, height); + + final width = [label.size, slider.size, description.size, error.size].order(by: (size) => size.width).max!.width; + size = constraints.constrain(Size(width, height + error.size.height)); + } +} + +class _RenderVerticalSlider extends _RenderSlider { + _RenderVerticalSlider(super._style, super._stateStyle, super._layout, super._marks, super._mainAxisExtent); + + @override + void performLayout() { + final loosened = constraints.loosen(); + + // Layout parts, assuming top-left corner of track/origin is (0, 0). + final insets = _style.labelLayoutStyle.childPadding; + final (:label, :paddedTrack, :description, :error, :slider) = layoutParts( + loosened, + switch (_mainAxisExtent) { + final extent? => loosened.copyWith(maxHeight: extent + insets.top + insets.bottom), + null => loosened, + }, + ); + + // Calculate offset to move the top/left corner of track/origin from (0, 0), such that no part of the slider has a + // negative offset. + final boxes = [label, paddedTrack, description, error]; + final largest = boxes.order(by: (box) => box.size.width).max!; + boxes.remove(largest); + + // Check whether the marks are larger than the largest label/description/error. + final largestMiddleOffset = largest.size.bottomCenter(Offset.zero).dx; + final marksOffset = slider.marks.values.map((rect) => rect.left).where((x) => x.isNegative).minOrNull?.abs() ?? 0; + + final double middle; + final double largestOffset; + final Offset sliderOffset; + + if (largestMiddleOffset < marksOffset) { + middle = marksOffset; + sliderOffset = Offset(marksOffset, label.size.height); + largestOffset = marksOffset + paddedTrack.size.width / 2; + } else { + middle = largestMiddleOffset; + sliderOffset = Offset(largestMiddleOffset - paddedTrack.size.width / 2, label.size.height); + largestOffset = 0.0; + } + + // Center align the slider's parts. + largest.data.offset = Offset(max(largestOffset - largest.size.width / 2, 0), 0); + for (final box in boxes) { + box.data.offset = Offset(middle - box.size.width / 2, 0); + } + + paddedTrack.data.offset = sliderOffset; + for (final MapEntry(key: label, value: rect) in slider.marks.entries) { + label.data.offset = rect.topLeft + sliderOffset; + } + + var height = label.size.height + slider.size.height; + description.data.offset = Offset(description.data.offset.dx, height); + + height += description.size.height; + error.data.offset = Offset(error.data.offset.dx, height); + + final width = max(largest.size.width, (sliderOffset & slider.size).right); + size = constraints.constrain(Size(width, height + error.size.height)); + } +} + +class _Data extends ContainerBoxParentData with ContainerParentDataMixin {} + +typedef _Parts = ({ + RenderBox paddedTrack, + RenderBox label, + RenderBox description, + RenderBox error, + ({Map marks, Size size}) slider, +}); + +abstract class _RenderSlider extends RenderBox + with ContainerRenderObjectMixin, RenderBoxContainerDefaultsMixin { + FSliderStyle _style; + FSliderStateStyle _stateStyle; + Layout _layout; + List _marks; + double? _mainAxisExtent; + late Rect Function(RenderBox, Size, FSliderMark, FSliderMarkStyle) _positionMark; + + _RenderSlider(this._style, this._stateStyle, this._layout, this._marks, this._mainAxisExtent) { + _positionMark = _position; + } + + @override + void setupParentData(covariant RenderObject child) => child.parentData = _Data(); + + _Parts layoutParts(BoxConstraints constraints, BoxConstraints trackMainAxis) { + final paddedTrack = firstChild!..layout(trackMainAxis, parentUsesSize: true); + + final label = childAfter(paddedTrack)!..layout(constraints, parentUsesSize: true); + final description = childAfter(label)!..layout(constraints, parentUsesSize: true); + final error = childAfter(description)!..layout(constraints, parentUsesSize: true); + + final (marks, sliderSize) = _layoutMarks(constraints, paddedTrack, childAfter(error)); + + return ( + paddedTrack: paddedTrack, + label: label, + description: description, + error: error, + slider: (marks: marks, size: sliderSize), + ); + } + + (Map, Size) _layoutMarks(BoxConstraints constraints, RenderBox paddedTrack, RenderBox? label) { + final marks = {}; + var minX = 0.0; + var minY = 0.0; + var maxX = paddedTrack.size.width; + var maxY = paddedTrack.size.height; + + final positionMark = _positionMark; + for (final mark in _marks) { + if (label == null) { + break; + } + + label.layout(constraints, parentUsesSize: true); + + final rect = positionMark(paddedTrack, label.size, mark, mark.style ?? _stateStyle.markStyle); + marks[label] = rect; + + minX = min(minX, rect.left); + maxX = max(maxX, rect.right); + minY = min(minY, rect.top); + maxY = max(maxY, rect.bottom); + + label = childAfter(label); + } + + return (marks, Size(maxX - minX, maxY - minY)); + } + + Rect Function(RenderBox, Size, FSliderMark, FSliderMarkStyle) get _position { + final insets = _style.labelLayoutStyle.childPadding; + return switch (_layout) { + Layout.ltr => (track, label, mark, style) { + final extent = track.size.width - insets.left - insets.right; + final offset = _anchor(extent, mark.value, insets.left, insets.top, style); + return _rect(label, mark, Offset(offset.$1, offset.$2), style); + }, + Layout.rtl => (track, size, mark, style) { + final extent = track.size.width - insets.left - insets.right; + final offset = _anchor(extent, 1 - mark.value, insets.left, insets.top, style); + return _rect(size, mark, Offset(offset.$1, offset.$2), style); + }, + Layout.ttb => (track, size, mark, style) { + final extent = track.size.height - insets.top - insets.bottom; + final offset = _anchor(extent, mark.value, insets.top, insets.left, style); + return _rect(size, mark, Offset(offset.$2, offset.$1), style); + }, + Layout.btt => (track, size, mark, style) { + final extent = track.size.height - insets.top - insets.bottom; + final offset = _anchor(extent, 1 - mark.value, insets.top, insets.left, style); + return _rect(size, mark, Offset(offset.$2, offset.$1), style); + }, + }; + } + + (double, double) _anchor( + double extent, + double offset, + double mainAxisPadding, + double crossAxisPadding, + FSliderMarkStyle markStyle, + ) { + final thumb = _style.thumbSize; + final trackMainAxis = (extent - thumb) * offset; + final anchorMainAxis = (thumb / 2) + trackMainAxis + mainAxisPadding; + + final crossAxisExtent = _style.crossAxisExtent < thumb ? thumb : _style.crossAxisExtent; + final crossAxisOffset = crossAxisPadding + (markStyle.labelOffset < 0 ? 0.0 : crossAxisExtent); + final anchorCrossAxis = markStyle.labelOffset + crossAxisOffset; + + return (anchorMainAxis, anchorCrossAxis); + } + + Rect _rect(Size size, FSliderMark mark, Offset anchor, FSliderMarkStyle markStyle) { + final rect = anchor & size; + return rect.shift(anchor - markStyle.labelAnchor.relative(to: size, origin: anchor)); + } + + @override + void paint(PaintingContext context, Offset offset) { + // We paint the labels first, then the track so that the thumb is painted on top of the labels. + var child = lastChild; + while (child != null) { + final childParentData = child.parentData! as _Data; + context.paintChild(child, childParentData.offset + offset); + child = childParentData.previousSibling; + } + } + + @override + bool hitTestChildren(BoxHitTestResult result, {required Offset position}) => + defaultHitTestChildren(result, position: position); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('style', style)) + ..add(DiagnosticsProperty('stateStyle', stateStyle)) + ..add(EnumProperty('layout', sliderLayout)) + ..add(IterableProperty('marks', marks)) + ..add(DoubleProperty('mainAxisExtent', mainAxisExtent)); + } + + FSliderStyle get style => _style; + + set style(FSliderStyle value) { + if (_style == value) { + return; + } + + _style = value; + _positionMark = _position; + markNeedsLayout(); + } + + FSliderStateStyle get stateStyle => _stateStyle; + + set stateStyle(FSliderStateStyle value) { + if (_stateStyle == value) { + return; + } + + _stateStyle = value; + markNeedsLayout(); + } + + Layout get sliderLayout => _layout; + + set sliderLayout(Layout value) { + if (_layout == value) { + return; + } + + _layout = value; + _positionMark = _position; + markNeedsLayout(); + } + + List get marks => _marks; + + set marks(List value) { + if (_marks.equals(value)) { + return; + } + + _marks = value; + markNeedsLayout(); + } + + double? get mainAxisExtent => _mainAxisExtent; + + set mainAxisExtent(double? value) { + if (_mainAxisExtent == value) { + return; + } + + _mainAxisExtent = value; + markNeedsLayout(); + } +} diff --git a/forui/lib/src/widgets/slider/slider_styles.dart b/forui/lib/src/widgets/slider/slider_styles.dart new file mode 100644 index 000000000..7f781b33a --- /dev/null +++ b/forui/lib/src/widgets/slider/slider_styles.dart @@ -0,0 +1,448 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/rendering.dart'; + +import 'package:meta/meta.dart'; + +import 'package:forui/forui.dart'; +import 'package:forui/src/foundation/tappable.dart'; + +/// A slider's styles. +final class FSliderStyles with Diagnosticable { + /// The enabled slider's horizontal style. + final FSliderStyle horizontalStyle; + + /// The enabled slider's vertical style. + final FSliderStyle verticalStyle; + + /// Creates a [FSliderStyles]. + FSliderStyles({ + required this.horizontalStyle, + required this.verticalStyle, + }); + + /// Creates a [FSliderStyles] that inherits its properties from the given [FColorScheme]. + factory FSliderStyles.inherit({ + required FColorScheme colorScheme, + required FTypography typography, + required FStyle style, + }) { + final enabledHorizontalStyle = FSliderStateStyle( + labelTextStyle: style.enabledFormFieldStyle.labelTextStyle, + descriptionTextStyle: style.enabledFormFieldStyle.descriptionTextStyle, + activeColor: colorScheme.primary, + inactiveColor: colorScheme.secondary, + markStyle: FSliderMarkStyle( + tickColor: colorScheme.mutedForeground, + labelTextStyle: typography.xs.copyWith(color: colorScheme.mutedForeground), + labelAnchor: Alignment.topCenter, + labelOffset: 10, + ), + tooltipStyle: FTooltipStyle.inherit(colorScheme: colorScheme, typography: typography, style: style), + thumbStyle: FSliderThumbStyle( + color: colorScheme.primaryForeground, + borderColor: colorScheme.primary, + ), + ); + + final disabledHorizontalStyle = FSliderStateStyle( + labelTextStyle: style.disabledFormFieldStyle.labelTextStyle, + descriptionTextStyle: style.disabledFormFieldStyle.descriptionTextStyle, + activeColor: colorScheme.disable(colorScheme.primary), + inactiveColor: colorScheme.secondary, + markStyle: FSliderMarkStyle( + tickColor: colorScheme.mutedForeground, + labelTextStyle: typography.xs.copyWith(color: colorScheme.disable(colorScheme.mutedForeground)), + labelAnchor: Alignment.topCenter, + labelOffset: 10, + ), + tooltipStyle: FTooltipStyle.inherit(colorScheme: colorScheme, typography: typography, style: style), + thumbStyle: FSliderThumbStyle( + color: colorScheme.primaryForeground, + borderColor: colorScheme.disable(colorScheme.primary), + ), + ); + + final errorHorizontalStyle = FSliderErrorStyle( + labelTextStyle: style.errorFormFieldStyle.labelTextStyle, + descriptionTextStyle: style.errorFormFieldStyle.descriptionTextStyle, + errorTextStyle: style.errorFormFieldStyle.errorTextStyle, + activeColor: colorScheme.error, + inactiveColor: colorScheme.secondary, + markStyle: FSliderMarkStyle( + tickColor: colorScheme.mutedForeground, + labelTextStyle: typography.xs.copyWith(color: colorScheme.error), + labelAnchor: Alignment.topCenter, + labelOffset: 10, + ), + tooltipStyle: FTooltipStyle.inherit(colorScheme: colorScheme, typography: typography, style: style), + thumbStyle: FSliderThumbStyle( + color: colorScheme.errorForeground, + borderColor: colorScheme.error, + ), + ); + + return FSliderStyles( + horizontalStyle: FSliderStyle( + labelLayoutStyle: const FLabelLayoutStyle( + labelPadding: EdgeInsets.only(bottom: 5), + childPadding: EdgeInsets.only(top: 10, bottom: 20, left: 10, right: 10), + descriptionPadding: EdgeInsets.only(top: 10), + errorPadding: EdgeInsets.only(top: 5), + ), + enabledStyle: enabledHorizontalStyle, + disabledStyle: disabledHorizontalStyle, + errorStyle: errorHorizontalStyle, + ), + verticalStyle: FSliderStyle( + labelLayoutStyle: const FLabelLayoutStyle( + labelPadding: EdgeInsets.only(bottom: 5), + childPadding: EdgeInsets.all(10), + descriptionPadding: EdgeInsets.only(top: 5), + errorPadding: EdgeInsets.only(top: 5), + ), + enabledStyle: enabledHorizontalStyle.copyWith( + markStyle: FSliderMarkStyle( + tickColor: colorScheme.mutedForeground, + labelTextStyle: typography.xs.copyWith(color: colorScheme.mutedForeground), + labelAnchor: Alignment.centerRight, + labelOffset: -10, + ), + ), + disabledStyle: disabledHorizontalStyle.copyWith( + markStyle: FSliderMarkStyle( + tickColor: colorScheme.mutedForeground, + labelTextStyle: typography.xs.copyWith(color: colorScheme.mutedForeground.withOpacity(0.7)), + labelAnchor: Alignment.centerRight, + labelOffset: -10, + ), + ), + errorStyle: errorHorizontalStyle.copyWith( + markStyle: FSliderMarkStyle( + tickColor: colorScheme.mutedForeground, + labelTextStyle: typography.xs.copyWith(color: colorScheme.mutedForeground), + labelAnchor: Alignment.centerRight, + labelOffset: -10, + ), + ), + tooltipTipAnchor: Touch.primary ? Alignment.bottomCenter : Alignment.centerLeft, + tooltipThumbAnchor: Touch.primary ? Alignment.topCenter : Alignment.centerRight, + ), + ); + } + + /// Returns a copy of this [FSliderStyles] but with the given fields replaced with the new values. + @useResult + FSliderStyles copyWith({ + FSliderStyle? horizontalStyle, + FSliderStyle? verticalStyle, + }) => + FSliderStyles( + horizontalStyle: horizontalStyle ?? this.horizontalStyle, + verticalStyle: verticalStyle ?? this.verticalStyle, + ); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('horizontalStyle', horizontalStyle)) + ..add(DiagnosticsProperty('verticalStyle', verticalStyle)); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is FSliderStyles && + runtimeType == other.runtimeType && + horizontalStyle == other.horizontalStyle && + verticalStyle == other.verticalStyle; + + @override + int get hashCode => horizontalStyle.hashCode ^ verticalStyle.hashCode; +} + +/// A slider's style. +final class FSliderStyle with Diagnosticable { + /// The label's layout style. + final FLabelLayoutStyle labelLayoutStyle; + + /// The enabled slider's style. + final FSliderStateStyle enabledStyle; + + /// The disabled slider's style. + final FSliderStateStyle disabledStyle; + + /// The error slider's style. + final FSliderErrorStyle errorStyle; + + /// The slider's cross-axis extent. Defaults to 8. + /// + /// ## Contract: + /// Throws [AssertionError] if it is not positive. + final double crossAxisExtent; + + /// The thumb's size, inclusive of . Defaults to `25` on touch platforms and `20` on non-touch platforms. + /// + /// ## Contract + /// Throws [AssertionError] if [thumbSize] is not positive. + /// + /// ## Implementation details + /// This unfortunately has to be placed outside of FSliderThumbStyle because [FSliderThumbStyle] is inside + /// [FSliderStateStyle]. Putting the thumb size inside [FSliderThumbStyle] will cause a cyclic rebuild to occur + /// whenever the window is resized due to some bad interaction between an internal LayoutBuilder and SliderFormField. + final double thumbSize; + + /// The anchor of the tooltip to which the [tooltipThumbAnchor] is aligned to. + /// + /// Defaults to [Alignment.bottomCenter] on primarily touch devices and [Alignment.centerLeft] on non-primarily touch + /// devices. + final Alignment tooltipTipAnchor; + + /// The anchor of the thumb to which the [tooltipTipAnchor] is aligned to. + /// + /// Defaults to [Alignment.topCenter] on primarily touch devices and [Alignment.centerRight] on non-primarily touch + /// devices. + final Alignment tooltipThumbAnchor; + + /// Creates a [FSliderStyle]. + FSliderStyle({ + required this.labelLayoutStyle, + required this.enabledStyle, + required this.disabledStyle, + required this.errorStyle, + this.crossAxisExtent = 8, + double? thumbSize, + this.tooltipTipAnchor = Alignment.bottomCenter, + this.tooltipThumbAnchor = Alignment.topCenter, + }) : assert(thumbSize == null || 0 < thumbSize, 'The thumb size must be positive'), + thumbSize = thumbSize ?? (Touch.primary ? 25 : 20); + + /// Returns a copy of this [FSliderStyle] but with the given fields replaced with the new values. + @useResult + FSliderStyle copyWith({ + FLabelLayoutStyle? labelLayoutStyle, + FSliderStateStyle? enabledStyle, + FSliderStateStyle? disabledStyle, + FSliderErrorStyle? errorStyle, + double? thumbSize, + double? crossAxisExtent, + Alignment? tooltipTipAnchor, + Alignment? tooltipThumbAnchor, + }) => + FSliderStyle( + labelLayoutStyle: labelLayoutStyle ?? this.labelLayoutStyle, + enabledStyle: enabledStyle ?? this.enabledStyle, + disabledStyle: disabledStyle ?? this.disabledStyle, + errorStyle: errorStyle ?? this.errorStyle, + thumbSize: thumbSize ?? this.thumbSize, + crossAxisExtent: crossAxisExtent ?? this.crossAxisExtent, + tooltipTipAnchor: tooltipTipAnchor ?? this.tooltipTipAnchor, + tooltipThumbAnchor: tooltipThumbAnchor ?? this.tooltipThumbAnchor, + ); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('labelLayoutStyle', labelLayoutStyle)) + ..add(DiagnosticsProperty('enabledStyle', enabledStyle)) + ..add(DiagnosticsProperty('disabledStyle', disabledStyle)) + ..add(DiagnosticsProperty('errorStyle', errorStyle)) + ..add(DiagnosticsProperty('thumbSize', thumbSize)) + ..add(DoubleProperty('crossAxisExtent', crossAxisExtent)) + ..add(DiagnosticsProperty('tooltipTipAnchor', tooltipTipAnchor)) + ..add(DiagnosticsProperty('tooltipThumbAnchor', tooltipThumbAnchor)); + } + + /// The label style. + // ignore: diagnostic_describe_all_properties + FLabelStyle get labelStyle => ( + layout: labelLayoutStyle, + state: FLabelStateStyles( + enabledStyle: enabledStyle, + disabledStyle: disabledStyle, + errorStyle: errorStyle, + ), + ); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is FSliderStyle && + runtimeType == other.runtimeType && + labelLayoutStyle == other.labelLayoutStyle && + enabledStyle == other.enabledStyle && + disabledStyle == other.disabledStyle && + errorStyle == other.errorStyle && + crossAxisExtent == other.crossAxisExtent && + tooltipTipAnchor == other.tooltipTipAnchor && + tooltipThumbAnchor == other.tooltipThumbAnchor; + + @override + int get hashCode => + labelLayoutStyle.hashCode ^ + enabledStyle.hashCode ^ + disabledStyle.hashCode ^ + errorStyle.hashCode ^ + crossAxisExtent.hashCode ^ + tooltipTipAnchor.hashCode ^ + tooltipThumbAnchor.hashCode; +} + +/// A slider state's style. +// ignore: avoid_implementing_value_types +final class FSliderStateStyle with Diagnosticable implements FFormFieldStyle { + /// The label's [TextStyle]. + @override + final TextStyle labelTextStyle; + + /// The help/error's [TextStyle]. + @override + final TextStyle descriptionTextStyle; + + /// The slider's active color. + final Color activeColor; + + /// The slider inactive color. + final Color inactiveColor; + + /// The slider's border radius. + final BorderRadius borderRadius; + + /// The slider marks' style. + final FSliderMarkStyle markStyle; + + /// The slider thumb's style. + final FSliderThumbStyle thumbStyle; + + /// The tooltip's style. + final FTooltipStyle tooltipStyle; + + /// Creates a [FSliderStateStyle]. + FSliderStateStyle({ + required this.labelTextStyle, + required this.descriptionTextStyle, + required this.activeColor, + required this.inactiveColor, + required this.markStyle, + required this.thumbStyle, + required this.tooltipStyle, + this.borderRadius = const BorderRadius.all(Radius.circular(4)), + }); + + /// Returns a copy of this [FSliderStateStyle] but with the given fields replaced with the new values. + @useResult + @override + FSliderStateStyle copyWith({ + TextStyle? labelTextStyle, + TextStyle? descriptionTextStyle, + Color? activeColor, + Color? inactiveColor, + double? mainAxisPadding, + BorderRadius? borderRadius, + FSliderMarkStyle? markStyle, + FSliderThumbStyle? thumbStyle, + FTooltipStyle? tooltipStyle, + }) => + FSliderStateStyle( + labelTextStyle: labelTextStyle ?? this.labelTextStyle, + descriptionTextStyle: descriptionTextStyle ?? this.descriptionTextStyle, + activeColor: activeColor ?? this.activeColor, + inactiveColor: inactiveColor ?? this.inactiveColor, + borderRadius: borderRadius ?? this.borderRadius, + markStyle: markStyle ?? this.markStyle, + thumbStyle: thumbStyle ?? this.thumbStyle, + tooltipStyle: tooltipStyle ?? this.tooltipStyle, + ); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('labelTextStyle', labelTextStyle)) + ..add(DiagnosticsProperty('descriptionTextStyle', descriptionTextStyle)) + ..add(ColorProperty('activeColor', activeColor)) + ..add(ColorProperty('inactiveColor', inactiveColor)) + ..add(DiagnosticsProperty('borderRadius', borderRadius)) + ..add(DiagnosticsProperty('markStyle', markStyle)) + ..add(DiagnosticsProperty('thumbStyle', thumbStyle)) + ..add(DiagnosticsProperty('tooltipStyle', tooltipStyle)); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is FSliderStateStyle && + runtimeType == other.runtimeType && + labelTextStyle == other.labelTextStyle && + descriptionTextStyle == other.descriptionTextStyle && + activeColor == other.activeColor && + inactiveColor == other.inactiveColor && + borderRadius == other.borderRadius && + markStyle == other.markStyle && + thumbStyle == other.thumbStyle && + tooltipStyle == other.tooltipStyle; + + @override + int get hashCode => + activeColor.hashCode ^ + inactiveColor.hashCode ^ + borderRadius.hashCode ^ + markStyle.hashCode ^ + thumbStyle.hashCode ^ + tooltipStyle.hashCode; +} + +/// A slider error's style. +// ignore: avoid_implementing_value_types +final class FSliderErrorStyle extends FSliderStateStyle implements FFormFieldErrorStyle { + /// The error's [TextStyle]. + @override + final TextStyle errorTextStyle; + + /// Creates a [FSliderErrorStyle]. + FSliderErrorStyle({ + required this.errorTextStyle, + required super.labelTextStyle, + required super.descriptionTextStyle, + required super.activeColor, + required super.inactiveColor, + required super.markStyle, + required super.thumbStyle, + required super.tooltipStyle, + super.borderRadius, + }); + + /// Returns a copy of this [FSliderStateStyle] but with the given fields replaced with the new values. + @useResult + @override + FSliderErrorStyle copyWith({ + TextStyle? labelTextStyle, + TextStyle? descriptionTextStyle, + TextStyle? errorTextStyle, + Color? activeColor, + Color? inactiveColor, + double? mainAxisPadding, + BorderRadius? borderRadius, + FSliderMarkStyle? markStyle, + FSliderThumbStyle? thumbStyle, + FTooltipStyle? tooltipStyle, + }) => + FSliderErrorStyle( + labelTextStyle: labelTextStyle ?? this.labelTextStyle, + descriptionTextStyle: descriptionTextStyle ?? this.descriptionTextStyle, + errorTextStyle: errorTextStyle ?? this.errorTextStyle, + activeColor: activeColor ?? this.activeColor, + inactiveColor: inactiveColor ?? this.inactiveColor, + borderRadius: borderRadius ?? this.borderRadius, + markStyle: markStyle ?? this.markStyle, + thumbStyle: thumbStyle ?? this.thumbStyle, + tooltipStyle: tooltipStyle ?? this.tooltipStyle, + ); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('errorTextStyle', errorTextStyle)); + } +} diff --git a/forui/lib/src/widgets/slider/slider_tooltips_controller.dart b/forui/lib/src/widgets/slider/slider_tooltips_controller.dart index 2e6a7312e..a3d476ae6 100644 --- a/forui/lib/src/widgets/slider/slider_tooltips_controller.dart +++ b/forui/lib/src/widgets/slider/slider_tooltips_controller.dart @@ -58,9 +58,15 @@ final class FSliderTooltipsController { } /// Removes the tooltip from the slider. - void remove(UniqueKey? key) { - if (enabled) { + void remove(UniqueKey? key, [FTooltipController? controller]) { + if (!enabled) { + return; + } + + if (controller == null) { _tooltips.remove(key); + } else { + _tooltips.removeWhere((key, controller) => key == key && controller == controller); } } } diff --git a/forui/lib/src/widgets/slider/thumb.dart b/forui/lib/src/widgets/slider/thumb.dart index 858fa69f5..221530fa9 100644 --- a/forui/lib/src/widgets/slider/thumb.dart +++ b/forui/lib/src/widgets/slider/thumb.dart @@ -7,6 +7,7 @@ import 'package:meta/meta.dart'; import 'package:forui/forui.dart'; import 'package:forui/src/widgets/slider/inherited_controller.dart'; import 'package:forui/src/widgets/slider/inherited_data.dart'; +import 'package:forui/src/widgets/slider/inherited_state.dart'; class _ShrinkIntent extends Intent { const _ShrinkIntent(); @@ -53,11 +54,15 @@ class _ThumbState extends State with SingleTickerProviderStateMixin { @override void didChangeDependencies() { super.didChangeDependencies(); - _controller = InheritedController.of(context)..tooltips.add(_key, _tooltip); + _controller = InheritedController.of(context); + // _controller.tooltips.remove(_key, _tooltip); + // _key = widget.min ? FSliderTooltipsController.min : FSliderTooltipsController.max; + _controller.tooltips.add(_key, _tooltip); } @override Widget build(BuildContext context) { + final thumbStyle = InheritedState.of(context).style.thumbStyle; final InheritedData(:style, :layout, :tooltipBuilder, :semanticValueFormatterCallback, :enabled) = InheritedData.of(context); @@ -90,14 +95,14 @@ class _ThumbState extends State with SingleTickerProviderStateMixin { child: DecoratedBox( decoration: BoxDecoration( shape: BoxShape.circle, - color: style.thumbStyle.color, + color: thumbStyle.color, border: Border.all( - color: style.thumbStyle.borderColor, - width: style.thumbStyle.borderWidth, + color: thumbStyle.borderColor, + width: thumbStyle.borderWidth, ), ), child: SizedBox.square( - dimension: style.thumbStyle.size, + dimension: style.thumbSize, ), ), ), @@ -159,7 +164,7 @@ class _ThumbState extends State with SingleTickerProviderStateMixin { onTapDown: down, onTapUp: up, onVerticalDragStart: start, - onVerticalDragUpdate: _drag(_controller, style, layout), + onVerticalDragUpdate: _drag(_controller, style.thumbSize, layout), onVerticalDragEnd: end, child: thumb, ); @@ -168,7 +173,7 @@ class _ThumbState extends State with SingleTickerProviderStateMixin { onTapDown: down, onTapUp: up, onHorizontalDragStart: start, - onHorizontalDragUpdate: _drag(_controller, style, layout), + onHorizontalDragUpdate: _drag(_controller, style.thumbSize, layout), onHorizontalDragEnd: end, child: thumb, ); @@ -196,12 +201,12 @@ class _ThumbState extends State with SingleTickerProviderStateMixin { }, }; - GestureDragUpdateCallback? _drag(FSliderController controller, FSliderStyle style, Layout layout) { + GestureDragUpdateCallback? _drag(FSliderController controller, double thumbSize, Layout layout) { if (controller.allowedInteraction == FSliderInteraction.tap) { return null; } - final translate = layout.translateThumbDrag(style); + final translate = layout.translateThumbDrag(thumbSize); void drag(DragUpdateDetails details) { final origin = widget.min ? _origin!.min : _origin!.max; @@ -220,16 +225,14 @@ class _ThumbState extends State with SingleTickerProviderStateMixin { } /// A slider thumb's style. +/// +/// **Note**: +/// The thumb size can be configured inside [FSliderStyle] instead. This is due to an unfortunate limitation of the +/// implementation. final class FSliderThumbStyle with Diagnosticable { /// The thumb's color. final Color color; - /// The thumb's size, inclusive of [borderWidth]. Defaults to `20`. - /// - /// ## Contract - /// Throws [AssertionError] if [size] is not positive. - final double size; - /// The border's color. final Color borderColor; @@ -243,22 +246,18 @@ final class FSliderThumbStyle with Diagnosticable { FSliderThumbStyle({ required this.color, required this.borderColor, - this.size = 20, this.borderWidth = 2, - }) : assert(0 < size, 'The diameter must be positive'), - assert(0 < borderWidth, 'The border width must be positive'); + }) : assert(0 < borderWidth, 'The border width must be positive'); /// Returns a copy of this [FSliderThumbStyle] but with the given fields replaced with the new values. @useResult FSliderThumbStyle copyWith({ Color? color, - double? size, Color? borderColor, double? borderWidth, }) => FSliderThumbStyle( color: color ?? this.color, - size: size ?? this.size, borderColor: borderColor ?? this.borderColor, borderWidth: borderWidth ?? this.borderWidth, ); @@ -268,7 +267,6 @@ final class FSliderThumbStyle with Diagnosticable { super.debugFillProperties(properties); properties ..add(ColorProperty('color', color)) - ..add(DoubleProperty('size', size)) ..add(ColorProperty('borderColor', borderColor)) ..add(DoubleProperty('borderWidth', borderWidth)); } @@ -279,20 +277,19 @@ final class FSliderThumbStyle with Diagnosticable { other is FSliderThumbStyle && runtimeType == other.runtimeType && color == other.color && - size == other.size && borderColor == other.borderColor && borderWidth == other.borderWidth; @override - int get hashCode => color.hashCode ^ size.hashCode ^ borderColor.hashCode ^ borderWidth.hashCode; + int get hashCode => color.hashCode ^ borderColor.hashCode ^ borderWidth.hashCode; } @internal extension Layouts on Layout { - double Function(Offset) translateThumbDrag(FSliderStyle style) => switch (this) { - Layout.ltr => (delta) => delta.dx - style.thumbStyle.size / 2, - Layout.rtl => (delta) => -delta.dx + style.thumbStyle.size / 2, - Layout.ttb => (delta) => delta.dy - style.thumbStyle.size / 2, - Layout.btt => (delta) => -delta.dy + style.thumbStyle.size / 2, + double Function(Offset) translateThumbDrag(double thumbSize) => switch (this) { + Layout.ltr => (delta) => delta.dx - thumbSize / 2, + Layout.rtl => (delta) => -delta.dx + thumbSize / 2, + Layout.ttb => (delta) => delta.dy - thumbSize / 2, + Layout.btt => (delta) => -delta.dy + thumbSize / 2, }; } diff --git a/forui/lib/src/widgets/slider/track.dart b/forui/lib/src/widgets/slider/track.dart index abd2bcec5..d56b15fe1 100644 --- a/forui/lib/src/widgets/slider/track.dart +++ b/forui/lib/src/widgets/slider/track.dart @@ -5,8 +5,10 @@ import 'package:flutter/widgets.dart'; import 'package:meta/meta.dart'; import 'package:forui/forui.dart'; +import 'package:forui/src/foundation/tappable.dart'; import 'package:forui/src/widgets/slider/inherited_controller.dart'; import 'package:forui/src/widgets/slider/inherited_data.dart'; +import 'package:forui/src/widgets/slider/inherited_state.dart'; import 'package:forui/src/widgets/slider/thumb.dart'; @internal @@ -15,31 +17,37 @@ class Track extends StatelessWidget { @override Widget build(BuildContext context) { - final InheritedData(:style, :layout) = InheritedData.of(context); + final InheritedData(:style, :layout, :semanticFormatterCallback) = InheritedData.of(context); final controller = InheritedController.of(context); final position = layout.position; - final crossAxisExtent = max(style.thumbStyle.size, style.crossAxisExtent); + final crossAxisExtent = max(style.thumbSize, style.crossAxisExtent); final (height, width) = layout.vertical ? (null, crossAxisExtent) : (crossAxisExtent, null); return SizedBox( height: height, width: width, - child: Stack( - alignment: Alignment.center, - children: [ - const _GestureDetector(), - if (controller.extendable.min) - position( - offset: controller.selection.offset.min * controller.selection.rawExtent.total, - child: const Thumb(min: true), - ), - if (controller.extendable.max) - position( - offset: controller.selection.offset.max * controller.selection.rawExtent.total, - child: const Thumb(min: false), - ), - ], + child: Semantics( + container: true, + slider: true, + enabled: true, + value: semanticFormatterCallback(controller.selection), + child: Stack( + alignment: Alignment.center, + children: [ + const _GestureDetector(), + if (controller.extendable.min) + position( + offset: controller.selection.offset.min * controller.selection.rawExtent.total, + child: const Thumb(min: true), + ), + if (controller.extendable.max) + position( + offset: controller.selection.offset.max * controller.selection.rawExtent.total, + child: const Thumb(min: false), + ), + ], + ), ), ); } @@ -60,25 +68,23 @@ class _GestureDetectorState extends State<_GestureDetector> { @override Widget build(BuildContext context) { - final InheritedData(:style, :layout, :enabled, :semanticFormatterCallback) = InheritedData.of(context); + final InheritedData(:style, :layout, :trackHitRegionCrossExtent, :enabled) = InheritedData.of(context); final controller = InheritedController.of(context); - final track = Semantics( - slider: true, - enabled: enabled, - value: semanticFormatterCallback(controller.selection), - child: const _Track(), - ); + Widget track = const Center(child: _Track()); if (!enabled) { return track; } - if (controller.allowedInteraction != FSliderInteraction.slide) { - return GestureDetector( - onTapDown: _tap(controller, style, layout), - onTapUp: (_) => controller.tooltips.hide(), - onTapCancel: controller.tooltips.hide, + if (Touch.primary || trackHitRegionCrossExtent != null) { + final crossAxisExtent = trackHitRegionCrossExtent ?? max(style.thumbSize, style.crossAxisExtent); + final (height, width) = layout.vertical ? (null, crossAxisExtent) : (crossAxisExtent, null); + + track = Container( + height: height, + width: width, + color: const Color(0x00000000), child: track, ); } @@ -97,27 +103,27 @@ class _GestureDetectorState extends State<_GestureDetector> { if (layout.vertical) { return GestureDetector( - onTapDown: _tap(controller, style, layout), + onTapDown: _tap(controller, style.thumbSize, layout), onTapUp: (_) => controller.tooltips.hide(), onVerticalDragStart: start, - onVerticalDragUpdate: _drag(controller, style, layout), + onVerticalDragUpdate: _drag(controller, layout), onVerticalDragEnd: end, child: track, ); } else { return GestureDetector( - onTapDown: _tap(controller, style, layout), + onTapDown: _tap(controller, style.thumbSize, layout), onTapUp: (_) => controller.tooltips.hide(), onHorizontalDragStart: start, - onHorizontalDragUpdate: _drag(controller, style, layout), + onHorizontalDragUpdate: _drag(controller, layout), onHorizontalDragEnd: end, child: track, ); } } - GestureTapDownCallback? _tap(FSliderController controller, FSliderStyle style, Layout layout) { - final translate = layout.translateTrackTap(controller.selection.rawExtent.total, style); + GestureTapDownCallback? _tap(FSliderController controller, double thumbSize, Layout layout) { + final translate = layout.translateTrackTap(controller.selection.rawExtent.total, thumbSize); void down(TapDownDetails details) { final offset = switch (translate(details.localPosition)) { @@ -138,7 +144,7 @@ class _GestureDetectorState extends State<_GestureDetector> { return tappable.contains(controller.allowedInteraction) ? down : null; } - GestureDragUpdateCallback? _drag(FSliderController controller, FSliderStyle style, Layout layout) { + GestureDragUpdateCallback? _drag(FSliderController controller, Layout layout) { if (controller.allowedInteraction != FSliderInteraction.slide) { return null; } @@ -148,14 +154,12 @@ class _GestureDetectorState extends State<_GestureDetector> { 'Slider must be extendable at one edge when ${controller.allowedInteraction}.', ); - final translate = layout.translateTrackDrag(style); + final translate = layout.translateTrackDrag(); - void drag(DragUpdateDetails details) { + return (details) { final origin = controller.extendable.min ? _origin!.min : _origin!.max; controller.slide(origin + translate(details.localPosition - _pointerOrigin!), min: controller.extendable.min); - } - - return drag; + }; } } @@ -165,12 +169,13 @@ class _Track extends StatelessWidget { @override Widget build(BuildContext context) { final InheritedData(:style, :layout, :marks, :enabled) = InheritedData.of(context); - final FSliderStyle(:inactiveColor, :borderRadius, :crossAxisExtent, :markStyle, :thumbStyle) = style; + final FSliderStateStyle(:inactiveColor, :borderRadius, :markStyle, :thumbStyle) = InheritedState.of(context).style; + final crossAxisExtent = style.crossAxisExtent; final extent = InheritedController.of(context, InheritedController.rawExtent).selection.rawExtent.total; final position = layout.position; - final half = thumbStyle.size / 2; + final half = style.thumbSize / 2; final (height, width) = layout.vertical ? (null, crossAxisExtent) : (crossAxisExtent, null); return DecoratedBox( @@ -213,10 +218,11 @@ class ActiveTrack extends StatelessWidget { @override Widget build(BuildContext context) { final InheritedData(:style, :layout) = InheritedData.of(context); - final FSliderStyle(:activeColor, :borderRadius, :crossAxisExtent, :thumbStyle) = style; + final FSliderStateStyle(:activeColor, :borderRadius, :thumbStyle) = InheritedState.of(context).style; + final crossAxisExtent = style.crossAxisExtent; final rawOffset = InheritedController.of(context, InheritedController.rawOffset).selection.rawOffset; - final mainAxisExtent = rawOffset.max - rawOffset.min + thumbStyle.size / 2; + final mainAxisExtent = rawOffset.max - rawOffset.min + style.thumbSize / 2; final (height, width) = layout.vertical ? (mainAxisExtent, crossAxisExtent) : (crossAxisExtent, mainAxisExtent); return layout.position( @@ -237,14 +243,14 @@ class ActiveTrack extends StatelessWidget { @internal extension Layouts on Layout { - double Function(Offset) translateTrackTap(double extent, FSliderStyle style) => switch (this) { - Layout.ltr => (offset) => offset.dx - style.thumbStyle.size / 2, - Layout.rtl => (offset) => extent - offset.dx + style.thumbStyle.size / 2, - Layout.ttb => (offset) => offset.dy - style.thumbStyle.size / 2, - Layout.btt => (offset) => extent - offset.dy + style.thumbStyle.size / 2, + double Function(Offset) translateTrackTap(double extent, double thumbSize) => switch (this) { + Layout.ltr => (offset) => offset.dx - thumbSize / 2, + Layout.rtl => (offset) => extent - offset.dx + thumbSize / 2, + Layout.ttb => (offset) => offset.dy - thumbSize / 2, + Layout.btt => (offset) => extent - offset.dy + thumbSize / 2, }; - double Function(Offset) translateTrackDrag(FSliderStyle style) => switch (this) { + double Function(Offset) translateTrackDrag() => switch (this) { Layout.ltr => (delta) => delta.dx, Layout.rtl => (delta) => -delta.dx, Layout.ttb => (delta) => delta.dy, diff --git a/forui/lib/src/widgets/switch.dart b/forui/lib/src/widgets/switch.dart index d06e30e1f..88c05a285 100644 --- a/forui/lib/src/widgets/switch.dart +++ b/forui/lib/src/widgets/switch.dart @@ -221,9 +221,10 @@ final class FSwitchStyle with Diagnosticable { ); /// The [FLabel]'s style. + // ignore: diagnostic_describe_all_properties FLabelStyle get labelStyle => ( layout: labelLayoutStyle, - state: FLabelStateStyle( + state: FLabelStateStyles( enabledStyle: enabledStyle, disabledStyle: disabledStyle, errorStyle: errorStyle, @@ -255,8 +256,7 @@ final class FSwitchStyle with Diagnosticable { ..add(DiagnosticsProperty('labelLayoutStyle', labelLayoutStyle)) ..add(DiagnosticsProperty('enabledStyle', enabledStyle)) ..add(DiagnosticsProperty('disabledStyle', disabledStyle)) - ..add(DiagnosticsProperty('errorStyle', errorStyle)) - ..add(DiagnosticsProperty('labelStyle', labelStyle)); + ..add(DiagnosticsProperty('errorStyle', errorStyle)); } @override diff --git a/forui/lib/src/widgets/text_field/field.dart b/forui/lib/src/widgets/text_field/field.dart index 5b72d023e..0d6429acb 100644 --- a/forui/lib/src/widgets/text_field/field.dart +++ b/forui/lib/src/widgets/text_field/field.dart @@ -50,7 +50,7 @@ class Field extends FormField { required FTextFieldStyle style, super.key, }) : super( - onSaved: parent.onSave, + onSaved: parent.onSaved, validator: parent.validator, initialValue: parent.initialValue, enabled: parent.enabled, diff --git a/forui/lib/src/widgets/text_field/text_field.dart b/forui/lib/src/widgets/text_field/text_field.dart index ad7414655..f6df7d7e2 100644 --- a/forui/lib/src/widgets/text_field/text_field.dart +++ b/forui/lib/src/widgets/text_field/text_field.dart @@ -460,7 +460,7 @@ final class FTextField extends StatelessWidget { final Widget? suffix; /// An optional method to call with the final value when the form is saved via [FormState.save]. - final FormFieldSetter? onSave; + final FormFieldSetter? onSaved; /// An optional method that validates an input. Returns an error string to /// display if the input is invalid, or null otherwise. @@ -554,7 +554,7 @@ final class FTextField extends StatelessWidget { this.undoController, this.spellCheckConfiguration, this.suffix, - this.onSave, + this.onSaved, this.validator, this.initialValue, this.autovalidateMode, @@ -615,7 +615,7 @@ final class FTextField extends StatelessWidget { this.undoController, this.spellCheckConfiguration, this.suffix, - this.onSave, + this.onSaved, this.validator, this.initialValue, this.autovalidateMode, @@ -679,7 +679,7 @@ final class FTextField extends StatelessWidget { this.undoController, this.spellCheckConfiguration, this.suffix, - this.onSave, + this.onSaved, this.validator, this.initialValue, this.autovalidateMode, @@ -744,7 +744,7 @@ final class FTextField extends StatelessWidget { this.undoController, this.spellCheckConfiguration, this.suffix, - this.onSave, + this.onSaved, this.validator, this.initialValue, this.autovalidateMode, @@ -855,7 +855,7 @@ final class FTextField extends StatelessWidget { ..add(DiagnosticsProperty('undoController', undoController)) ..add(DiagnosticsProperty('spellCheckConfiguration', spellCheckConfiguration)) ..add(DiagnosticsProperty('suffixIcon', suffix)) - ..add(ObjectFlagProperty.has('onSave', onSave)) + ..add(ObjectFlagProperty.has('onSaved', onSaved)) ..add(ObjectFlagProperty.has('validator', validator)) ..add(StringProperty('initialValue', initialValue)) ..add(EnumProperty('autovalidateMode', autovalidateMode)) diff --git a/forui/lib/src/widgets/text_field/text_field_style.dart b/forui/lib/src/widgets/text_field/text_field_style.dart index 062999f54..d2f639eda 100644 --- a/forui/lib/src/widgets/text_field/text_field_style.dart +++ b/forui/lib/src/widgets/text_field/text_field_style.dart @@ -118,9 +118,10 @@ final class FTextFieldStyle with Diagnosticable { ); /// The label style. + // ignore: diagnostic_describe_all_properties FLabelStyle get labelStyle => ( layout: labelLayoutStyle, - state: FLabelStateStyle( + state: FLabelStateStyles( enabledStyle: enabledStyle, disabledStyle: disabledStyle, errorStyle: errorStyle, @@ -136,7 +137,6 @@ final class FTextFieldStyle with Diagnosticable { ..add(DiagnosticsProperty('contentPadding', contentPadding)) ..add(DiagnosticsProperty('scrollPadding', scrollPadding)) ..add(DiagnosticsProperty('labelLayoutStyle', labelLayoutStyle)) - ..add(DiagnosticsProperty('labelStyle', labelStyle)) ..add(DiagnosticsProperty('enabledStyle', enabledStyle)) ..add(DiagnosticsProperty('disabledStyle', disabledStyle)) ..add(DiagnosticsProperty('errorStyle', errorStyle)); diff --git a/forui/lib/widgets/slider.dart b/forui/lib/widgets/slider.dart index 289ee723d..aea57fe4e 100644 --- a/forui/lib/widgets/slider.dart +++ b/forui/lib/widgets/slider.dart @@ -9,5 +9,6 @@ export '../src/widgets/slider/slider.dart'; export '../src/widgets/slider/slider_controller.dart'; export '../src/widgets/slider/slider_mark.dart'; export '../src/widgets/slider/slider_selection.dart' show FSliderSelection; +export '../src/widgets/slider/slider_styles.dart'; export '../src/widgets/slider/slider_tooltips_controller.dart'; export '../src/widgets/slider/thumb.dart' hide Layouts, Thumb; diff --git a/forui/pubspec.yaml b/forui/pubspec.yaml index 794750a19..3424f3717 100644 --- a/forui/pubspec.yaml +++ b/forui/pubspec.yaml @@ -36,12 +36,6 @@ dev_dependencies: sdk: flutter mockito: ^5.4.4 -#dependency_overrides: -# forui_assets: -# path: ../forui_assets - - - # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/forui/test/golden/popover/zinc-dark-hidden.png b/forui/test/golden/popover/hidden-zinc-dark.png similarity index 100% rename from forui/test/golden/popover/zinc-dark-hidden.png rename to forui/test/golden/popover/hidden-zinc-dark.png diff --git a/forui/test/golden/popover/zinc-light-hidden.png b/forui/test/golden/popover/hidden-zinc-light.png similarity index 100% rename from forui/test/golden/popover/zinc-light-hidden.png rename to forui/test/golden/popover/hidden-zinc-light.png diff --git a/forui/test/golden/popover/shown-non-touch-device-zinc-dark.png b/forui/test/golden/popover/shown-non-touch-device-zinc-dark.png new file mode 100644 index 000000000..38e557fbd Binary files /dev/null and b/forui/test/golden/popover/shown-non-touch-device-zinc-dark.png differ diff --git a/forui/test/golden/popover/shown-non-touch-device-zinc-light.png b/forui/test/golden/popover/shown-non-touch-device-zinc-light.png new file mode 100644 index 000000000..f44384bd9 Binary files /dev/null and b/forui/test/golden/popover/shown-non-touch-device-zinc-light.png differ diff --git a/forui/test/golden/popover/zinc-dark-shown.png b/forui/test/golden/popover/shown-touch-device-zinc-dark.png similarity index 100% rename from forui/test/golden/popover/zinc-dark-shown.png rename to forui/test/golden/popover/shown-touch-device-zinc-dark.png diff --git a/forui/test/golden/popover/zinc-light-shown.png b/forui/test/golden/popover/shown-touch-device-zinc-light.png similarity index 100% rename from forui/test/golden/popover/zinc-light-shown.png rename to forui/test/golden/popover/shown-touch-device-zinc-light.png diff --git a/forui/test/golden/slider/interweaving-marks.png b/forui/test/golden/slider/interweaving-marks.png new file mode 100644 index 000000000..2a0655398 Binary files /dev/null and b/forui/test/golden/slider/interweaving-marks.png differ diff --git a/forui/test/golden/slider/label-offset-Layout.btt--20.0.png b/forui/test/golden/slider/label-offset-Layout.btt--20.0.png deleted file mode 100644 index 8dacb0785..000000000 Binary files a/forui/test/golden/slider/label-offset-Layout.btt--20.0.png and /dev/null differ diff --git a/forui/test/golden/slider/label-offset-Layout.btt-20.0.png b/forui/test/golden/slider/label-offset-Layout.btt-20.0.png deleted file mode 100644 index 338d744d6..000000000 Binary files a/forui/test/golden/slider/label-offset-Layout.btt-20.0.png and /dev/null differ diff --git a/forui/test/golden/slider/label-offset-Layout.ltr--20.0.png b/forui/test/golden/slider/label-offset-Layout.ltr--20.0.png deleted file mode 100644 index 8b528706e..000000000 Binary files a/forui/test/golden/slider/label-offset-Layout.ltr--20.0.png and /dev/null differ diff --git a/forui/test/golden/slider/label-offset-Layout.ltr-20.0.png b/forui/test/golden/slider/label-offset-Layout.ltr-20.0.png deleted file mode 100644 index 246e0c3cb..000000000 Binary files a/forui/test/golden/slider/label-offset-Layout.ltr-20.0.png and /dev/null differ diff --git a/forui/test/golden/slider/label-offset-Layout.rtl--20.0.png b/forui/test/golden/slider/label-offset-Layout.rtl--20.0.png deleted file mode 100644 index 6c1104bdc..000000000 Binary files a/forui/test/golden/slider/label-offset-Layout.rtl--20.0.png and /dev/null differ diff --git a/forui/test/golden/slider/label-offset-Layout.rtl-20.0.png b/forui/test/golden/slider/label-offset-Layout.rtl-20.0.png deleted file mode 100644 index 13902aa1b..000000000 Binary files a/forui/test/golden/slider/label-offset-Layout.rtl-20.0.png and /dev/null differ diff --git a/forui/test/golden/slider/label-offset-Layout.ttb--20.0.png b/forui/test/golden/slider/label-offset-Layout.ttb--20.0.png deleted file mode 100644 index c69b213d5..000000000 Binary files a/forui/test/golden/slider/label-offset-Layout.ttb--20.0.png and /dev/null differ diff --git a/forui/test/golden/slider/label-offset-Layout.ttb-20.0.png b/forui/test/golden/slider/label-offset-Layout.ttb-20.0.png deleted file mode 100644 index 926a60843..000000000 Binary files a/forui/test/golden/slider/label-offset-Layout.ttb-20.0.png and /dev/null differ diff --git a/forui/test/golden/slider/label-offset/Layout.btt-asymmetric.png b/forui/test/golden/slider/label-offset/Layout.btt-asymmetric.png new file mode 100644 index 000000000..dae84fcde Binary files /dev/null and b/forui/test/golden/slider/label-offset/Layout.btt-asymmetric.png differ diff --git a/forui/test/golden/slider/label-offset/Layout.btt-labelled.png b/forui/test/golden/slider/label-offset/Layout.btt-labelled.png new file mode 100644 index 000000000..947d2bd38 Binary files /dev/null and b/forui/test/golden/slider/label-offset/Layout.btt-labelled.png differ diff --git a/forui/test/golden/slider/label-offset/Layout.btt-symmetric.png b/forui/test/golden/slider/label-offset/Layout.btt-symmetric.png new file mode 100644 index 000000000..29f70c3bf Binary files /dev/null and b/forui/test/golden/slider/label-offset/Layout.btt-symmetric.png differ diff --git a/forui/test/golden/slider/label-offset/Layout.ltr-asymmetric.png b/forui/test/golden/slider/label-offset/Layout.ltr-asymmetric.png new file mode 100644 index 000000000..28a68cb5b Binary files /dev/null and b/forui/test/golden/slider/label-offset/Layout.ltr-asymmetric.png differ diff --git a/forui/test/golden/slider/label-offset/Layout.ltr-labelled.png b/forui/test/golden/slider/label-offset/Layout.ltr-labelled.png new file mode 100644 index 000000000..e21e96395 Binary files /dev/null and b/forui/test/golden/slider/label-offset/Layout.ltr-labelled.png differ diff --git a/forui/test/golden/slider/label-offset/Layout.ltr-symmetric.png b/forui/test/golden/slider/label-offset/Layout.ltr-symmetric.png new file mode 100644 index 000000000..ac53f84d9 Binary files /dev/null and b/forui/test/golden/slider/label-offset/Layout.ltr-symmetric.png differ diff --git a/forui/test/golden/slider/label-offset/Layout.rtl-asymmetric.png b/forui/test/golden/slider/label-offset/Layout.rtl-asymmetric.png new file mode 100644 index 000000000..103e827d9 Binary files /dev/null and b/forui/test/golden/slider/label-offset/Layout.rtl-asymmetric.png differ diff --git a/forui/test/golden/slider/label-offset/Layout.rtl-labelled.png b/forui/test/golden/slider/label-offset/Layout.rtl-labelled.png new file mode 100644 index 000000000..3efc51655 Binary files /dev/null and b/forui/test/golden/slider/label-offset/Layout.rtl-labelled.png differ diff --git a/forui/test/golden/slider/label-offset/Layout.rtl-symmetric.png b/forui/test/golden/slider/label-offset/Layout.rtl-symmetric.png new file mode 100644 index 000000000..5a258f668 Binary files /dev/null and b/forui/test/golden/slider/label-offset/Layout.rtl-symmetric.png differ diff --git a/forui/test/golden/slider/label-offset/Layout.ttb-asymmetric.png b/forui/test/golden/slider/label-offset/Layout.ttb-asymmetric.png new file mode 100644 index 000000000..ad0b510f8 Binary files /dev/null and b/forui/test/golden/slider/label-offset/Layout.ttb-asymmetric.png differ diff --git a/forui/test/golden/slider/label-offset/Layout.ttb-labelled.png b/forui/test/golden/slider/label-offset/Layout.ttb-labelled.png new file mode 100644 index 000000000..20a69f68d Binary files /dev/null and b/forui/test/golden/slider/label-offset/Layout.ttb-labelled.png differ diff --git a/forui/test/golden/slider/label-offset/Layout.ttb-symmetric.png b/forui/test/golden/slider/label-offset/Layout.ttb-symmetric.png new file mode 100644 index 000000000..a6d740e5b Binary files /dev/null and b/forui/test/golden/slider/label-offset/Layout.ttb-symmetric.png differ diff --git a/forui/test/golden/slider/range-slider-zinc-dark-Layout.btt-disabled.png b/forui/test/golden/slider/range-slider-zinc-dark-Layout.btt-disabled.png deleted file mode 100644 index 8832b41ac..000000000 Binary files a/forui/test/golden/slider/range-slider-zinc-dark-Layout.btt-disabled.png and /dev/null differ diff --git a/forui/test/golden/slider/range-slider-zinc-dark-Layout.btt-enabled.png b/forui/test/golden/slider/range-slider-zinc-dark-Layout.btt-enabled.png deleted file mode 100644 index 568e826bf..000000000 Binary files a/forui/test/golden/slider/range-slider-zinc-dark-Layout.btt-enabled.png and /dev/null differ diff --git a/forui/test/golden/slider/range-slider-zinc-dark-Layout.ltr-disabled.png b/forui/test/golden/slider/range-slider-zinc-dark-Layout.ltr-disabled.png deleted file mode 100644 index 07a437e6f..000000000 Binary files a/forui/test/golden/slider/range-slider-zinc-dark-Layout.ltr-disabled.png and /dev/null differ diff --git a/forui/test/golden/slider/range-slider-zinc-dark-Layout.ltr-enabled.png b/forui/test/golden/slider/range-slider-zinc-dark-Layout.ltr-enabled.png deleted file mode 100644 index b40e67755..000000000 Binary files a/forui/test/golden/slider/range-slider-zinc-dark-Layout.ltr-enabled.png and /dev/null differ diff --git a/forui/test/golden/slider/range-slider-zinc-dark-Layout.rtl-disabled.png b/forui/test/golden/slider/range-slider-zinc-dark-Layout.rtl-disabled.png deleted file mode 100644 index 3baa32bd6..000000000 Binary files a/forui/test/golden/slider/range-slider-zinc-dark-Layout.rtl-disabled.png and /dev/null differ diff --git a/forui/test/golden/slider/range-slider-zinc-dark-Layout.rtl-enabled.png b/forui/test/golden/slider/range-slider-zinc-dark-Layout.rtl-enabled.png deleted file mode 100644 index c9ffafc3d..000000000 Binary files a/forui/test/golden/slider/range-slider-zinc-dark-Layout.rtl-enabled.png and /dev/null differ diff --git a/forui/test/golden/slider/range-slider-zinc-dark-Layout.ttb-disabled.png b/forui/test/golden/slider/range-slider-zinc-dark-Layout.ttb-disabled.png deleted file mode 100644 index 6fa7b038c..000000000 Binary files a/forui/test/golden/slider/range-slider-zinc-dark-Layout.ttb-disabled.png and /dev/null differ diff --git a/forui/test/golden/slider/range-slider-zinc-dark-Layout.ttb-enabled.png b/forui/test/golden/slider/range-slider-zinc-dark-Layout.ttb-enabled.png deleted file mode 100644 index f86557d84..000000000 Binary files a/forui/test/golden/slider/range-slider-zinc-dark-Layout.ttb-enabled.png and /dev/null differ diff --git a/forui/test/golden/slider/range-slider-zinc-light-Layout.btt-disabled.png b/forui/test/golden/slider/range-slider-zinc-light-Layout.btt-disabled.png deleted file mode 100644 index 0c4077849..000000000 Binary files a/forui/test/golden/slider/range-slider-zinc-light-Layout.btt-disabled.png and /dev/null differ diff --git a/forui/test/golden/slider/range-slider-zinc-light-Layout.btt-enabled.png b/forui/test/golden/slider/range-slider-zinc-light-Layout.btt-enabled.png deleted file mode 100644 index 5e5790d1d..000000000 Binary files a/forui/test/golden/slider/range-slider-zinc-light-Layout.btt-enabled.png and /dev/null differ diff --git a/forui/test/golden/slider/range-slider-zinc-light-Layout.ltr-disabled.png b/forui/test/golden/slider/range-slider-zinc-light-Layout.ltr-disabled.png deleted file mode 100644 index a68c8c708..000000000 Binary files a/forui/test/golden/slider/range-slider-zinc-light-Layout.ltr-disabled.png and /dev/null differ diff --git a/forui/test/golden/slider/range-slider-zinc-light-Layout.ltr-enabled.png b/forui/test/golden/slider/range-slider-zinc-light-Layout.ltr-enabled.png deleted file mode 100644 index f5219dc6f..000000000 Binary files a/forui/test/golden/slider/range-slider-zinc-light-Layout.ltr-enabled.png and /dev/null differ diff --git a/forui/test/golden/slider/range-slider-zinc-light-Layout.rtl-disabled.png b/forui/test/golden/slider/range-slider-zinc-light-Layout.rtl-disabled.png deleted file mode 100644 index f7d6e0c7e..000000000 Binary files a/forui/test/golden/slider/range-slider-zinc-light-Layout.rtl-disabled.png and /dev/null differ diff --git a/forui/test/golden/slider/range-slider-zinc-light-Layout.rtl-enabled.png b/forui/test/golden/slider/range-slider-zinc-light-Layout.rtl-enabled.png deleted file mode 100644 index 7e5457a53..000000000 Binary files a/forui/test/golden/slider/range-slider-zinc-light-Layout.rtl-enabled.png and /dev/null differ diff --git a/forui/test/golden/slider/range-slider-zinc-light-Layout.ttb-disabled.png b/forui/test/golden/slider/range-slider-zinc-light-Layout.ttb-disabled.png deleted file mode 100644 index ecc09801d..000000000 Binary files a/forui/test/golden/slider/range-slider-zinc-light-Layout.ttb-disabled.png and /dev/null differ diff --git a/forui/test/golden/slider/range-slider-zinc-light-Layout.ttb-enabled.png b/forui/test/golden/slider/range-slider-zinc-light-Layout.ttb-enabled.png deleted file mode 100644 index 0dfca162b..000000000 Binary files a/forui/test/golden/slider/range-slider-zinc-light-Layout.ttb-enabled.png and /dev/null differ diff --git a/forui/test/golden/slider/range-slider/zinc-dark-Layout.btt-desktop-disabled.png b/forui/test/golden/slider/range-slider/zinc-dark-Layout.btt-desktop-disabled.png new file mode 100644 index 000000000..f6a724582 Binary files /dev/null and b/forui/test/golden/slider/range-slider/zinc-dark-Layout.btt-desktop-disabled.png differ diff --git a/forui/test/golden/slider/range-slider/zinc-dark-Layout.btt-desktop-enabled.png b/forui/test/golden/slider/range-slider/zinc-dark-Layout.btt-desktop-enabled.png new file mode 100644 index 000000000..7d358350f Binary files /dev/null and b/forui/test/golden/slider/range-slider/zinc-dark-Layout.btt-desktop-enabled.png differ diff --git a/forui/test/golden/slider/range-slider/zinc-dark-Layout.btt-desktop-error.png b/forui/test/golden/slider/range-slider/zinc-dark-Layout.btt-desktop-error.png new file mode 100644 index 000000000..f701d32ab Binary files /dev/null and b/forui/test/golden/slider/range-slider/zinc-dark-Layout.btt-desktop-error.png differ diff --git a/forui/test/golden/slider/range-slider/zinc-dark-Layout.btt-touch-disabled.png b/forui/test/golden/slider/range-slider/zinc-dark-Layout.btt-touch-disabled.png new file mode 100644 index 000000000..e501bad31 Binary files /dev/null and b/forui/test/golden/slider/range-slider/zinc-dark-Layout.btt-touch-disabled.png differ diff --git a/forui/test/golden/slider/range-slider/zinc-dark-Layout.btt-touch-enabled.png b/forui/test/golden/slider/range-slider/zinc-dark-Layout.btt-touch-enabled.png new file mode 100644 index 000000000..5eee251b8 Binary files /dev/null and b/forui/test/golden/slider/range-slider/zinc-dark-Layout.btt-touch-enabled.png differ diff --git a/forui/test/golden/slider/range-slider/zinc-dark-Layout.btt-touch-error.png b/forui/test/golden/slider/range-slider/zinc-dark-Layout.btt-touch-error.png new file mode 100644 index 000000000..1775045cc Binary files /dev/null and b/forui/test/golden/slider/range-slider/zinc-dark-Layout.btt-touch-error.png differ diff --git a/forui/test/golden/slider/range-slider/zinc-dark-Layout.ltr-desktop-disabled.png b/forui/test/golden/slider/range-slider/zinc-dark-Layout.ltr-desktop-disabled.png new file mode 100644 index 000000000..664a415c9 Binary files /dev/null and b/forui/test/golden/slider/range-slider/zinc-dark-Layout.ltr-desktop-disabled.png differ diff --git a/forui/test/golden/slider/range-slider/zinc-dark-Layout.ltr-desktop-enabled.png b/forui/test/golden/slider/range-slider/zinc-dark-Layout.ltr-desktop-enabled.png new file mode 100644 index 000000000..4332d1ad6 Binary files /dev/null and b/forui/test/golden/slider/range-slider/zinc-dark-Layout.ltr-desktop-enabled.png differ diff --git a/forui/test/golden/slider/range-slider/zinc-dark-Layout.ltr-desktop-error.png b/forui/test/golden/slider/range-slider/zinc-dark-Layout.ltr-desktop-error.png new file mode 100644 index 000000000..4b0369039 Binary files /dev/null and b/forui/test/golden/slider/range-slider/zinc-dark-Layout.ltr-desktop-error.png differ diff --git a/forui/test/golden/slider/range-slider/zinc-dark-Layout.ltr-touch-disabled.png b/forui/test/golden/slider/range-slider/zinc-dark-Layout.ltr-touch-disabled.png new file mode 100644 index 000000000..ff2bd1316 Binary files /dev/null and b/forui/test/golden/slider/range-slider/zinc-dark-Layout.ltr-touch-disabled.png differ diff --git a/forui/test/golden/slider/range-slider/zinc-dark-Layout.ltr-touch-enabled.png b/forui/test/golden/slider/range-slider/zinc-dark-Layout.ltr-touch-enabled.png new file mode 100644 index 000000000..dc02681a4 Binary files /dev/null and b/forui/test/golden/slider/range-slider/zinc-dark-Layout.ltr-touch-enabled.png differ diff --git a/forui/test/golden/slider/range-slider/zinc-dark-Layout.ltr-touch-error.png b/forui/test/golden/slider/range-slider/zinc-dark-Layout.ltr-touch-error.png new file mode 100644 index 000000000..2fea83992 Binary files /dev/null and b/forui/test/golden/slider/range-slider/zinc-dark-Layout.ltr-touch-error.png differ diff --git a/forui/test/golden/slider/range-slider/zinc-dark-Layout.rtl-desktop-disabled.png b/forui/test/golden/slider/range-slider/zinc-dark-Layout.rtl-desktop-disabled.png new file mode 100644 index 000000000..0840238ae Binary files /dev/null and b/forui/test/golden/slider/range-slider/zinc-dark-Layout.rtl-desktop-disabled.png differ diff --git a/forui/test/golden/slider/range-slider/zinc-dark-Layout.rtl-desktop-enabled.png b/forui/test/golden/slider/range-slider/zinc-dark-Layout.rtl-desktop-enabled.png new file mode 100644 index 000000000..aa1047972 Binary files /dev/null and b/forui/test/golden/slider/range-slider/zinc-dark-Layout.rtl-desktop-enabled.png differ diff --git a/forui/test/golden/slider/range-slider/zinc-dark-Layout.rtl-desktop-error.png b/forui/test/golden/slider/range-slider/zinc-dark-Layout.rtl-desktop-error.png new file mode 100644 index 000000000..e0e3aaa82 Binary files /dev/null and b/forui/test/golden/slider/range-slider/zinc-dark-Layout.rtl-desktop-error.png differ diff --git a/forui/test/golden/slider/range-slider/zinc-dark-Layout.rtl-touch-disabled.png b/forui/test/golden/slider/range-slider/zinc-dark-Layout.rtl-touch-disabled.png new file mode 100644 index 000000000..b7c21ca58 Binary files /dev/null and b/forui/test/golden/slider/range-slider/zinc-dark-Layout.rtl-touch-disabled.png differ diff --git a/forui/test/golden/slider/range-slider/zinc-dark-Layout.rtl-touch-enabled.png b/forui/test/golden/slider/range-slider/zinc-dark-Layout.rtl-touch-enabled.png new file mode 100644 index 000000000..70969b77a Binary files /dev/null and b/forui/test/golden/slider/range-slider/zinc-dark-Layout.rtl-touch-enabled.png differ diff --git a/forui/test/golden/slider/range-slider/zinc-dark-Layout.rtl-touch-error.png b/forui/test/golden/slider/range-slider/zinc-dark-Layout.rtl-touch-error.png new file mode 100644 index 000000000..9832e9068 Binary files /dev/null and b/forui/test/golden/slider/range-slider/zinc-dark-Layout.rtl-touch-error.png differ diff --git a/forui/test/golden/slider/range-slider/zinc-dark-Layout.ttb-desktop-disabled.png b/forui/test/golden/slider/range-slider/zinc-dark-Layout.ttb-desktop-disabled.png new file mode 100644 index 000000000..97a5eba85 Binary files /dev/null and b/forui/test/golden/slider/range-slider/zinc-dark-Layout.ttb-desktop-disabled.png differ diff --git a/forui/test/golden/slider/range-slider/zinc-dark-Layout.ttb-desktop-enabled.png b/forui/test/golden/slider/range-slider/zinc-dark-Layout.ttb-desktop-enabled.png new file mode 100644 index 000000000..c52e4b246 Binary files /dev/null and b/forui/test/golden/slider/range-slider/zinc-dark-Layout.ttb-desktop-enabled.png differ diff --git a/forui/test/golden/slider/range-slider/zinc-dark-Layout.ttb-desktop-error.png b/forui/test/golden/slider/range-slider/zinc-dark-Layout.ttb-desktop-error.png new file mode 100644 index 000000000..10afda2cb Binary files /dev/null and b/forui/test/golden/slider/range-slider/zinc-dark-Layout.ttb-desktop-error.png differ diff --git a/forui/test/golden/slider/range-slider/zinc-dark-Layout.ttb-touch-disabled.png b/forui/test/golden/slider/range-slider/zinc-dark-Layout.ttb-touch-disabled.png new file mode 100644 index 000000000..dae3a1a98 Binary files /dev/null and b/forui/test/golden/slider/range-slider/zinc-dark-Layout.ttb-touch-disabled.png differ diff --git a/forui/test/golden/slider/range-slider/zinc-dark-Layout.ttb-touch-enabled.png b/forui/test/golden/slider/range-slider/zinc-dark-Layout.ttb-touch-enabled.png new file mode 100644 index 000000000..05ac3ad18 Binary files /dev/null and b/forui/test/golden/slider/range-slider/zinc-dark-Layout.ttb-touch-enabled.png differ diff --git a/forui/test/golden/slider/range-slider/zinc-dark-Layout.ttb-touch-error.png b/forui/test/golden/slider/range-slider/zinc-dark-Layout.ttb-touch-error.png new file mode 100644 index 000000000..73b59755d Binary files /dev/null and b/forui/test/golden/slider/range-slider/zinc-dark-Layout.ttb-touch-error.png differ diff --git a/forui/test/golden/slider/range-slider/zinc-light-Layout.btt-desktop-disabled.png b/forui/test/golden/slider/range-slider/zinc-light-Layout.btt-desktop-disabled.png new file mode 100644 index 000000000..b03eb3117 Binary files /dev/null and b/forui/test/golden/slider/range-slider/zinc-light-Layout.btt-desktop-disabled.png differ diff --git a/forui/test/golden/slider/range-slider/zinc-light-Layout.btt-desktop-enabled.png b/forui/test/golden/slider/range-slider/zinc-light-Layout.btt-desktop-enabled.png new file mode 100644 index 000000000..83fd043d8 Binary files /dev/null and b/forui/test/golden/slider/range-slider/zinc-light-Layout.btt-desktop-enabled.png differ diff --git a/forui/test/golden/slider/range-slider/zinc-light-Layout.btt-desktop-error.png b/forui/test/golden/slider/range-slider/zinc-light-Layout.btt-desktop-error.png new file mode 100644 index 000000000..478b16f34 Binary files /dev/null and b/forui/test/golden/slider/range-slider/zinc-light-Layout.btt-desktop-error.png differ diff --git a/forui/test/golden/slider/range-slider/zinc-light-Layout.btt-touch-disabled.png b/forui/test/golden/slider/range-slider/zinc-light-Layout.btt-touch-disabled.png new file mode 100644 index 000000000..2652ac705 Binary files /dev/null and b/forui/test/golden/slider/range-slider/zinc-light-Layout.btt-touch-disabled.png differ diff --git a/forui/test/golden/slider/range-slider/zinc-light-Layout.btt-touch-enabled.png b/forui/test/golden/slider/range-slider/zinc-light-Layout.btt-touch-enabled.png new file mode 100644 index 000000000..6a3b731ac Binary files /dev/null and b/forui/test/golden/slider/range-slider/zinc-light-Layout.btt-touch-enabled.png differ diff --git a/forui/test/golden/slider/range-slider/zinc-light-Layout.btt-touch-error.png b/forui/test/golden/slider/range-slider/zinc-light-Layout.btt-touch-error.png new file mode 100644 index 000000000..5d4868362 Binary files /dev/null and b/forui/test/golden/slider/range-slider/zinc-light-Layout.btt-touch-error.png differ diff --git a/forui/test/golden/slider/range-slider/zinc-light-Layout.ltr-desktop-disabled.png b/forui/test/golden/slider/range-slider/zinc-light-Layout.ltr-desktop-disabled.png new file mode 100644 index 000000000..b5cc71f55 Binary files /dev/null and b/forui/test/golden/slider/range-slider/zinc-light-Layout.ltr-desktop-disabled.png differ diff --git a/forui/test/golden/slider/range-slider/zinc-light-Layout.ltr-desktop-enabled.png b/forui/test/golden/slider/range-slider/zinc-light-Layout.ltr-desktop-enabled.png new file mode 100644 index 000000000..55b99a577 Binary files /dev/null and b/forui/test/golden/slider/range-slider/zinc-light-Layout.ltr-desktop-enabled.png differ diff --git a/forui/test/golden/slider/range-slider/zinc-light-Layout.ltr-desktop-error.png b/forui/test/golden/slider/range-slider/zinc-light-Layout.ltr-desktop-error.png new file mode 100644 index 000000000..e6a41bfdf Binary files /dev/null and b/forui/test/golden/slider/range-slider/zinc-light-Layout.ltr-desktop-error.png differ diff --git a/forui/test/golden/slider/range-slider/zinc-light-Layout.ltr-touch-disabled.png b/forui/test/golden/slider/range-slider/zinc-light-Layout.ltr-touch-disabled.png new file mode 100644 index 000000000..2bc64a64b Binary files /dev/null and b/forui/test/golden/slider/range-slider/zinc-light-Layout.ltr-touch-disabled.png differ diff --git a/forui/test/golden/slider/range-slider/zinc-light-Layout.ltr-touch-enabled.png b/forui/test/golden/slider/range-slider/zinc-light-Layout.ltr-touch-enabled.png new file mode 100644 index 000000000..3f53403b1 Binary files /dev/null and b/forui/test/golden/slider/range-slider/zinc-light-Layout.ltr-touch-enabled.png differ diff --git a/forui/test/golden/slider/range-slider/zinc-light-Layout.ltr-touch-error.png b/forui/test/golden/slider/range-slider/zinc-light-Layout.ltr-touch-error.png new file mode 100644 index 000000000..4d3184018 Binary files /dev/null and b/forui/test/golden/slider/range-slider/zinc-light-Layout.ltr-touch-error.png differ diff --git a/forui/test/golden/slider/range-slider/zinc-light-Layout.rtl-desktop-disabled.png b/forui/test/golden/slider/range-slider/zinc-light-Layout.rtl-desktop-disabled.png new file mode 100644 index 000000000..b550413b5 Binary files /dev/null and b/forui/test/golden/slider/range-slider/zinc-light-Layout.rtl-desktop-disabled.png differ diff --git a/forui/test/golden/slider/range-slider/zinc-light-Layout.rtl-desktop-enabled.png b/forui/test/golden/slider/range-slider/zinc-light-Layout.rtl-desktop-enabled.png new file mode 100644 index 000000000..84a70d145 Binary files /dev/null and b/forui/test/golden/slider/range-slider/zinc-light-Layout.rtl-desktop-enabled.png differ diff --git a/forui/test/golden/slider/range-slider/zinc-light-Layout.rtl-desktop-error.png b/forui/test/golden/slider/range-slider/zinc-light-Layout.rtl-desktop-error.png new file mode 100644 index 000000000..a7dcc15a6 Binary files /dev/null and b/forui/test/golden/slider/range-slider/zinc-light-Layout.rtl-desktop-error.png differ diff --git a/forui/test/golden/slider/range-slider/zinc-light-Layout.rtl-touch-disabled.png b/forui/test/golden/slider/range-slider/zinc-light-Layout.rtl-touch-disabled.png new file mode 100644 index 000000000..1c55cf9ba Binary files /dev/null and b/forui/test/golden/slider/range-slider/zinc-light-Layout.rtl-touch-disabled.png differ diff --git a/forui/test/golden/slider/range-slider/zinc-light-Layout.rtl-touch-enabled.png b/forui/test/golden/slider/range-slider/zinc-light-Layout.rtl-touch-enabled.png new file mode 100644 index 000000000..a9c9d9151 Binary files /dev/null and b/forui/test/golden/slider/range-slider/zinc-light-Layout.rtl-touch-enabled.png differ diff --git a/forui/test/golden/slider/range-slider/zinc-light-Layout.rtl-touch-error.png b/forui/test/golden/slider/range-slider/zinc-light-Layout.rtl-touch-error.png new file mode 100644 index 000000000..5e2e63298 Binary files /dev/null and b/forui/test/golden/slider/range-slider/zinc-light-Layout.rtl-touch-error.png differ diff --git a/forui/test/golden/slider/range-slider/zinc-light-Layout.ttb-desktop-disabled.png b/forui/test/golden/slider/range-slider/zinc-light-Layout.ttb-desktop-disabled.png new file mode 100644 index 000000000..8d1214d2e Binary files /dev/null and b/forui/test/golden/slider/range-slider/zinc-light-Layout.ttb-desktop-disabled.png differ diff --git a/forui/test/golden/slider/range-slider/zinc-light-Layout.ttb-desktop-enabled.png b/forui/test/golden/slider/range-slider/zinc-light-Layout.ttb-desktop-enabled.png new file mode 100644 index 000000000..deeef18db Binary files /dev/null and b/forui/test/golden/slider/range-slider/zinc-light-Layout.ttb-desktop-enabled.png differ diff --git a/forui/test/golden/slider/range-slider/zinc-light-Layout.ttb-desktop-error.png b/forui/test/golden/slider/range-slider/zinc-light-Layout.ttb-desktop-error.png new file mode 100644 index 000000000..aafee2f1e Binary files /dev/null and b/forui/test/golden/slider/range-slider/zinc-light-Layout.ttb-desktop-error.png differ diff --git a/forui/test/golden/slider/range-slider/zinc-light-Layout.ttb-touch-disabled.png b/forui/test/golden/slider/range-slider/zinc-light-Layout.ttb-touch-disabled.png new file mode 100644 index 000000000..340a17ec0 Binary files /dev/null and b/forui/test/golden/slider/range-slider/zinc-light-Layout.ttb-touch-disabled.png differ diff --git a/forui/test/golden/slider/range-slider/zinc-light-Layout.ttb-touch-enabled.png b/forui/test/golden/slider/range-slider/zinc-light-Layout.ttb-touch-enabled.png new file mode 100644 index 000000000..041face18 Binary files /dev/null and b/forui/test/golden/slider/range-slider/zinc-light-Layout.ttb-touch-enabled.png differ diff --git a/forui/test/golden/slider/range-slider/zinc-light-Layout.ttb-touch-error.png b/forui/test/golden/slider/range-slider/zinc-light-Layout.ttb-touch-error.png new file mode 100644 index 000000000..face1b75b Binary files /dev/null and b/forui/test/golden/slider/range-slider/zinc-light-Layout.ttb-touch-error.png differ diff --git a/forui/test/golden/slider/value-slider-Layout.btt-max.png b/forui/test/golden/slider/value-slider-Layout.btt-max.png deleted file mode 100644 index 26cd49eae..000000000 Binary files a/forui/test/golden/slider/value-slider-Layout.btt-max.png and /dev/null differ diff --git a/forui/test/golden/slider/value-slider-Layout.btt-min.png b/forui/test/golden/slider/value-slider-Layout.btt-min.png deleted file mode 100644 index aa2c8d6d8..000000000 Binary files a/forui/test/golden/slider/value-slider-Layout.btt-min.png and /dev/null differ diff --git a/forui/test/golden/slider/value-slider-Layout.ltr-max.png b/forui/test/golden/slider/value-slider-Layout.ltr-max.png deleted file mode 100644 index d3ff393ca..000000000 Binary files a/forui/test/golden/slider/value-slider-Layout.ltr-max.png and /dev/null differ diff --git a/forui/test/golden/slider/value-slider-Layout.ltr-min.png b/forui/test/golden/slider/value-slider-Layout.ltr-min.png deleted file mode 100644 index 3c02b8ade..000000000 Binary files a/forui/test/golden/slider/value-slider-Layout.ltr-min.png and /dev/null differ diff --git a/forui/test/golden/slider/value-slider-Layout.rtl-max.png b/forui/test/golden/slider/value-slider-Layout.rtl-max.png deleted file mode 100644 index b26c87061..000000000 Binary files a/forui/test/golden/slider/value-slider-Layout.rtl-max.png and /dev/null differ diff --git a/forui/test/golden/slider/value-slider-Layout.rtl-min.png b/forui/test/golden/slider/value-slider-Layout.rtl-min.png deleted file mode 100644 index 8816df7dd..000000000 Binary files a/forui/test/golden/slider/value-slider-Layout.rtl-min.png and /dev/null differ diff --git a/forui/test/golden/slider/value-slider-Layout.ttb-max.png b/forui/test/golden/slider/value-slider-Layout.ttb-max.png deleted file mode 100644 index 58277a578..000000000 Binary files a/forui/test/golden/slider/value-slider-Layout.ttb-max.png and /dev/null differ diff --git a/forui/test/golden/slider/value-slider-Layout.ttb-min.png b/forui/test/golden/slider/value-slider-Layout.ttb-min.png deleted file mode 100644 index 3800948ae..000000000 Binary files a/forui/test/golden/slider/value-slider-Layout.ttb-min.png and /dev/null differ diff --git a/forui/test/golden/slider/value-slider/Layout.btt-max.png b/forui/test/golden/slider/value-slider/Layout.btt-max.png new file mode 100644 index 000000000..53a509a2d Binary files /dev/null and b/forui/test/golden/slider/value-slider/Layout.btt-max.png differ diff --git a/forui/test/golden/slider/value-slider/Layout.btt-min.png b/forui/test/golden/slider/value-slider/Layout.btt-min.png new file mode 100644 index 000000000..4cdd9b7dd Binary files /dev/null and b/forui/test/golden/slider/value-slider/Layout.btt-min.png differ diff --git a/forui/test/golden/slider/value-slider/Layout.ltr-max.png b/forui/test/golden/slider/value-slider/Layout.ltr-max.png new file mode 100644 index 000000000..e0bda0cf2 Binary files /dev/null and b/forui/test/golden/slider/value-slider/Layout.ltr-max.png differ diff --git a/forui/test/golden/slider/value-slider/Layout.ltr-min.png b/forui/test/golden/slider/value-slider/Layout.ltr-min.png new file mode 100644 index 000000000..d357b7c85 Binary files /dev/null and b/forui/test/golden/slider/value-slider/Layout.ltr-min.png differ diff --git a/forui/test/golden/slider/value-slider/Layout.rtl-max.png b/forui/test/golden/slider/value-slider/Layout.rtl-max.png new file mode 100644 index 000000000..d05a618b2 Binary files /dev/null and b/forui/test/golden/slider/value-slider/Layout.rtl-max.png differ diff --git a/forui/test/golden/slider/value-slider/Layout.rtl-min.png b/forui/test/golden/slider/value-slider/Layout.rtl-min.png new file mode 100644 index 000000000..77afe159e Binary files /dev/null and b/forui/test/golden/slider/value-slider/Layout.rtl-min.png differ diff --git a/forui/test/golden/slider/value-slider/Layout.ttb-max.png b/forui/test/golden/slider/value-slider/Layout.ttb-max.png new file mode 100644 index 000000000..cc024a826 Binary files /dev/null and b/forui/test/golden/slider/value-slider/Layout.ttb-max.png differ diff --git a/forui/test/golden/slider/value-slider/Layout.ttb-min.png b/forui/test/golden/slider/value-slider/Layout.ttb-min.png new file mode 100644 index 000000000..02065b16c Binary files /dev/null and b/forui/test/golden/slider/value-slider/Layout.ttb-min.png differ diff --git a/forui/test/golden/tooltip/zinc-dark-hidden.png b/forui/test/golden/tooltip/hidden-zinc-dark.png similarity index 100% rename from forui/test/golden/tooltip/zinc-dark-hidden.png rename to forui/test/golden/tooltip/hidden-zinc-dark.png diff --git a/forui/test/golden/tooltip/zinc-light-hidden.png b/forui/test/golden/tooltip/hidden-zinc-light.png similarity index 100% rename from forui/test/golden/tooltip/zinc-light-hidden.png rename to forui/test/golden/tooltip/hidden-zinc-light.png diff --git a/forui/test/golden/tooltip/zinc-dark-shown.png b/forui/test/golden/tooltip/shown-zinc-dark.png similarity index 100% rename from forui/test/golden/tooltip/zinc-dark-shown.png rename to forui/test/golden/tooltip/shown-zinc-dark.png diff --git a/forui/test/golden/tooltip/zinc-light-shown.png b/forui/test/golden/tooltip/shown-zinc-light.png similarity index 100% rename from forui/test/golden/tooltip/zinc-light-shown.png rename to forui/test/golden/tooltip/shown-zinc-light.png diff --git a/forui/test/src/foundation/alignments_test.dart b/forui/test/src/foundation/rendering_test.dart similarity index 97% rename from forui/test/src/foundation/alignments_test.dart rename to forui/test/src/foundation/rendering_test.dart index ffb00ab07..4fdeee938 100644 --- a/forui/test/src/foundation/alignments_test.dart +++ b/forui/test/src/foundation/rendering_test.dart @@ -2,7 +2,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:forui/src/foundation/alignment.dart'; +import 'package:forui/src/foundation/rendering.dart'; void main() { group('Alignments', () { diff --git a/forui/test/src/foundation/tappable_test.dart b/forui/test/src/foundation/tappable_test.dart index 118479a32..8814b551b 100644 --- a/forui/test/src/foundation/tappable_test.dart +++ b/forui/test/src/foundation/tappable_test.dart @@ -21,11 +21,11 @@ void main() { ), ), ); - expect(find.text((focused: false, hovered: false, longPressed: false).toString()), findsOneWidget); + expect(find.text((focused: false, hovered: false, shortPressed: false).toString()), findsOneWidget); focusNode.requestFocus(); await tester.pumpAndSettle(); - expect(find.text((focused: true, hovered: false, longPressed: false).toString()), findsOneWidget); + expect(find.text((focused: true, hovered: false, shortPressed: false).toString()), findsOneWidget); }); testWidgets('hovered', (tester) async { @@ -37,7 +37,7 @@ void main() { ), ), ); - expect(find.text((focused: false, hovered: false, longPressed: false).toString()), findsOneWidget); + expect(find.text((focused: false, hovered: false, shortPressed: false).toString()), findsOneWidget); final gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(location: Offset.zero); @@ -47,12 +47,12 @@ void main() { await gesture.moveTo(tester.getCenter(find.byType(FTappable))); await tester.pumpAndSettle(); - expect(find.text((focused: false, hovered: true, longPressed: false).toString()), findsOneWidget); + expect(find.text((focused: false, hovered: true, shortPressed: false).toString()), findsOneWidget); await gesture.moveTo(Offset.zero); await tester.pumpAndSettle(); - expect(find.text((focused: false, hovered: false, longPressed: false).toString()), findsOneWidget); + expect(find.text((focused: false, hovered: false, shortPressed: false).toString()), findsOneWidget); }); testWidgets('long pressed', (tester) async { @@ -64,13 +64,13 @@ void main() { ), ), ); - expect(find.text((focused: false, hovered: false, longPressed: false).toString()), findsOneWidget); + expect(find.text((focused: false, hovered: false, shortPressed: false).toString()), findsOneWidget); await tester.longPress(find.byType(FTappable)); - expect(find.text((focused: false, hovered: false, longPressed: true).toString()), findsOneWidget); + expect(find.text((focused: false, hovered: false, shortPressed: true).toString()), findsOneWidget); await tester.pumpAndSettle(); - expect(find.text((focused: false, hovered: false, longPressed: false).toString()), findsOneWidget); + expect(find.text((focused: false, hovered: false, shortPressed: false).toString()), findsOneWidget); }); }); } diff --git a/forui/test/src/theme/color_scheme_test.dart b/forui/test/src/theme/color_scheme_test.dart index 22f257b8c..874dd97bb 100644 --- a/forui/test/src/theme/color_scheme_test.dart +++ b/forui/test/src/theme/color_scheme_test.dart @@ -9,6 +9,7 @@ void main() { group('FColorScheme', () { const scheme = FColorScheme( brightness: Brightness.light, + disabledColorLightness: 0.5, background: Colors.black, foreground: Colors.black12, primary: Colors.black26, @@ -30,6 +31,7 @@ void main() { test('all arguments', () { final copy = scheme.copyWith( brightness: Brightness.dark, + disabledColorLightness: 0.75, background: Colors.red, foreground: Colors.greenAccent, primary: Colors.yellow, @@ -46,6 +48,7 @@ void main() { ); expect(copy.brightness, equals(Brightness.dark)); + expect(copy.disabledColorLightness, equals(0.75)); expect(copy.background, equals(Colors.red)); expect(copy.foreground, equals(Colors.greenAccent)); expect(copy.primary, equals(Colors.yellow)); @@ -70,6 +73,7 @@ void main() { builder.properties.map((p) => p.toString()), [ EnumProperty('brightness', Brightness.light), + DoubleProperty('disabledColorLightness', 0.5), ColorProperty('background', Colors.black), ColorProperty('foreground', Colors.black12), ColorProperty('primary', Colors.black26), diff --git a/forui/test/src/theme/typography_test.dart b/forui/test/src/theme/typography_test.dart index 0474ac7c0..0ec2b226c 100644 --- a/forui/test/src/theme/typography_test.dart +++ b/forui/test/src/theme/typography_test.dart @@ -52,6 +52,7 @@ void main() { group('inherit constructor', () { const colorScheme = FColorScheme( brightness: Brightness.light, + disabledColorLightness: 0.5, background: Colors.black, foreground: Colors.black12, primary: Colors.black26, diff --git a/forui/test/src/widgets/popover/popover_golden_test.dart b/forui/test/src/widgets/popover/popover_golden_test.dart index 640a94ffe..91accef2d 100644 --- a/forui/test/src/widgets/popover/popover_golden_test.dart +++ b/forui/test/src/widgets/popover/popover_golden_test.dart @@ -8,6 +8,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:forui/forui.dart'; +import 'package:forui/src/foundation/tappable.dart'; import '../../test_scaffold.dart'; void main() { @@ -32,10 +33,11 @@ void main() { ), ); - await expectLater(find.byType(TestScaffold), matchesGoldenFile('popover/$name-hidden.png')); + await expectLater(find.byType(TestScaffold), matchesGoldenFile('popover/hidden-$name.png')); }); - testWidgets('$name shown', (tester) async { + testWidgets('$name shown on touch device', (tester) async { + Touch.primary = true; final controller = FPopoverController(vsync: const TestVSync()); await tester.pumpWidget( @@ -57,8 +59,36 @@ void main() { unawaited(controller.show()); await tester.pumpAndSettle(); - await expectLater(find.byType(TestScaffold), matchesGoldenFile('popover/$name-shown.png')); + await expectLater(find.byType(TestScaffold), matchesGoldenFile('popover/shown-touch-device-$name.png')); }); + + testWidgets('$name shown on non-touch device', (tester) async { + Touch.primary = false; + final controller = FPopoverController(vsync: const TestVSync()); + + await tester.pumpWidget( + TestScaffold.app( + data: theme, + child: FPopover( + controller: controller, + followerBuilder: (context, style, _) => const SizedBox.square(dimension: 100), + target: const ColoredBox( + color: Colors.yellow, + child: SizedBox.square( + dimension: 100, + ), + ), + ), + ), + ); + + unawaited(controller.show()); + await tester.pumpAndSettle(); + + await expectLater(find.byType(TestScaffold), matchesGoldenFile('popover/shown-non-touch-device-$name.png')); + }); + + tearDown(() => Touch.primary = null); }); } } diff --git a/forui/test/src/widgets/resizable/resizable_test.dart b/forui/test/src/widgets/resizable/resizable_test.dart index 19ff4d509..aae68204c 100644 --- a/forui/test/src/widgets/resizable/resizable_test.dart +++ b/forui/test/src/widgets/resizable/resizable_test.dart @@ -1,9 +1,9 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart' hide VerticalDivider; import 'package:flutter_test/flutter_test.dart'; import 'package:forui/forui.dart'; +import 'package:forui/src/foundation/tappable.dart'; import 'package:forui/src/widgets/resizable/divider.dart'; void main() { @@ -19,6 +19,10 @@ void main() { builder: (context, snapshot, child) => const Align(child: Text('B')), ); + setUp(() { + Touch.primary = false; + }); + for (final (index, constructor) in [ () => FResizable(crossAxisExtent: 0, axis: Axis.vertical, children: [top, bottom]), ].indexed) { @@ -26,8 +30,6 @@ void main() { } testWidgets('vertical drag downwards', (tester) async { - debugDefaultTargetPlatformOverride = TargetPlatform.linux; - final vertical = FResizable( crossAxisExtent: 50, axis: Axis.vertical, @@ -50,13 +52,9 @@ void main() { expect(tester.getSize(find.byType(FResizableRegion).first), const Size(50, 80)); expect(tester.getSize(find.byType(FResizableRegion).last), const Size(50, 20)); - - debugDefaultTargetPlatformOverride = null; // This cannot be called in tearDown, Flutter is dumb. }); testWidgets('vertical drag upwards', (tester) async { - debugDefaultTargetPlatformOverride = TargetPlatform.linux; - final vertical = FResizable( crossAxisExtent: 50, axis: Axis.vertical, @@ -85,13 +83,9 @@ void main() { tester.getSize(find.byType(FResizableRegion).last), const Size(50, 80), ); - - debugDefaultTargetPlatformOverride = null; // This cannot be called in tearDown, Flutter is dumb. }); testWidgets('horizontal drag right', (tester) async { - debugDefaultTargetPlatformOverride = TargetPlatform.linux; - final horizontal = FResizable( crossAxisExtent: 50, axis: Axis.horizontal, @@ -120,13 +114,9 @@ void main() { tester.getSize(find.byType(FResizableRegion).last), const Size(20, 50), ); - - debugDefaultTargetPlatformOverride = null; // This cannot be called in tearDown, Flutter is dumb. }); testWidgets('horizontal drag left', (tester) async { - debugDefaultTargetPlatformOverride = TargetPlatform.linux; - final horizontal = FResizable( crossAxisExtent: 50, axis: Axis.horizontal, @@ -155,7 +145,7 @@ void main() { tester.getSize(find.byType(FResizableRegion).last), const Size(80, 50), ); - - debugDefaultTargetPlatformOverride = null; // This cannot be called in tearDown, Flutter is dumb. }); + + tearDown(() => Touch.primary = null); } diff --git a/forui/test/src/widgets/slider/slider_controller_test.dart b/forui/test/src/widgets/slider/slider_controller_test.dart index 31d80a5ba..29213175e 100644 --- a/forui/test/src/widgets/slider/slider_controller_test.dart +++ b/forui/test/src/widgets/slider/slider_controller_test.dart @@ -13,9 +13,9 @@ void main() { ]; for (final constructor in [ - (selection, [FSliderInteraction? interaction, bool min = false]) => + (selection, [FSliderInteraction interaction = FSliderInteraction.tapAndSlideThumb, bool min = false]) => FContinuousSliderController(selection: selection, allowedInteraction: interaction, minExtendable: min), - (selection, [FSliderInteraction? interaction, bool min = false]) => + (selection, [FSliderInteraction interaction = FSliderInteraction.tapAndSlideThumb, bool min = false]) => FDiscreteSliderController(selection: selection, allowedInteraction: interaction, minExtendable: min), ]) { group('FSlider', () { diff --git a/forui/test/src/widgets/slider/slider_golden_test.dart b/forui/test/src/widgets/slider/slider_golden_test.dart index e9196a4fd..49e3b90c9 100644 --- a/forui/test/src/widgets/slider/slider_golden_test.dart +++ b/forui/test/src/widgets/slider/slider_golden_test.dart @@ -7,6 +7,7 @@ import 'package:flutter/material.dart' hide Thumb; import 'package:flutter_test/flutter_test.dart'; import 'package:forui/forui.dart'; +import 'package:forui/src/foundation/tappable.dart'; import 'package:forui/src/widgets/slider/thumb.dart'; import '../../test_scaffold.dart'; @@ -14,16 +15,74 @@ void main() { group('FSlider', () { for (final (name, theme, _) in TestScaffold.themes) { for (final layout in Layout.values) { - for (final enabled in [true, false]) { - testWidgets('$name - $layout - ${enabled ? 'enabled' : 'disabled'}', (tester) async { + for (final touch in [true, false]) { + for (final enabled in [true, false]) { + testWidgets('$name - $layout - ${enabled ? 'enabled' : 'disabled'}', (tester) async { + Touch.primary = touch; + final styles = FSliderStyles.inherit( + colorScheme: theme.colorScheme, + typography: theme.typography, + style: theme.style, + ); + + await tester.pumpWidget( + TestScaffold.app( + data: theme, + child: FSlider( + style: layout.vertical ? styles.verticalStyle : styles.horizontalStyle, + label: const Text('Label'), + description: const Text('Description'), + controller: FContinuousSliderController.range(selection: FSliderSelection(min: 0.30, max: 0.60)), + layout: layout, + enabled: enabled, + trackMainAxisExtent: 300, + marks: const [ + FSliderMark(value: 0.0, label: Text('0')), + FSliderMark(value: 0.25, label: Text('25'), tick: false), + FSliderMark(value: 0.5, label: Text('50')), + FSliderMark(value: 0.75, label: Text('75'), tick: false), + FSliderMark(value: 1.0, label: Text('100')), + ], + ), + ), + ); + + final gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + await gesture.addPointer(location: Offset.zero); + addTearDown(gesture.removePointer); + await tester.pump(); + + await gesture.moveTo(tester.getCenter(find.byType(Thumb).first)); + await tester.pumpAndSettle(const Duration(seconds: 1)); + + await expectLater( + find.byType(TestScaffold), + matchesGoldenFile( + 'slider/range-slider/$name-$layout-${touch ? 'touch' : 'desktop'}-${enabled ? 'enabled' : 'disabled'}.png', + ), + ); + }); + } + + testWidgets('$name - $layout - error', (tester) async { + Touch.primary = touch; + final styles = FSliderStyles.inherit( + colorScheme: theme.colorScheme, + typography: theme.typography, + style: theme.style, + ); + await tester.pumpWidget( TestScaffold.app( data: theme, - // background: background, child: FSlider( + style: layout.vertical ? styles.verticalStyle : styles.horizontalStyle, + label: const Text('Label'), + description: const Text('Description'), + forceErrorText: 'Error', controller: FContinuousSliderController.range(selection: FSliderSelection(min: 0.30, max: 0.60)), layout: layout, - enabled: enabled, + trackMainAxisExtent: 300, marks: const [ FSliderMark(value: 0.0, label: Text('0')), FSliderMark(value: 0.25, label: Text('25'), tick: false), @@ -45,7 +104,9 @@ void main() { await expectLater( find.byType(TestScaffold), - matchesGoldenFile('slider/range-slider-$name-$layout-${enabled ? 'enabled' : 'disabled'}.png'), + matchesGoldenFile( + 'slider/range-slider/$name-$layout-${touch ? 'touch' : 'desktop'}-error.png', + ), ); }); } @@ -58,7 +119,6 @@ void main() { await tester.pumpWidget( TestScaffold.app( data: FThemes.zinc.light, - // background: background, child: FSlider( controller: FContinuousSliderController( minExtendable: min, @@ -86,52 +146,156 @@ void main() { await expectLater( find.byType(TestScaffold), - matchesGoldenFile('slider/value-slider-$layout-${min ? 'min' : 'max'}.png'), + matchesGoldenFile('slider/value-slider/$layout-${min ? 'min' : 'max'}.png'), ); }); } } for (final layout in Layout.values) { - for (final labelOffset in [-20.0, 20.0]) { - testWidgets('label offset - $layout - $labelOffset', (tester) async { - final sliderStyle = FThemes.zinc.light.sliderStyles.enabledHorizontalStyle; - final style = sliderStyle.markStyle.copyWith(labelOffset: labelOffset); + group('label offset - $layout', () { + late FSliderStyle sliderStyle; + late Alignment positive; + late Alignment negative; + late List marks; + + setUp(() { + final sliderStyles = FThemes.zinc.light.sliderStyles; + sliderStyle = layout.vertical ? sliderStyles.verticalStyle : sliderStyles.horizontalStyle; + positive = layout.vertical ? Alignment.centerLeft : Alignment.topCenter; + negative = layout.vertical ? Alignment.centerRight : Alignment.bottomCenter; + marks = [ + FSliderMark( + value: 0.0, + label: const Text('0'), + style: sliderStyle.enabledStyle.markStyle.copyWith( + labelOffset: 20, + labelAnchor: positive, + ), + ), + FSliderMark( + value: 0.25, + label: const Text('25'), + style: sliderStyle.enabledStyle.markStyle.copyWith( + labelOffset: 1, + labelAnchor: positive, + ), + ), + FSliderMark( + value: 0.75, + label: const Text('75'), + style: sliderStyle.enabledStyle.markStyle.copyWith( + labelOffset: -1, + labelAnchor: negative, + ), + ), + FSliderMark( + value: 1.0, + label: const Text('100'), + style: sliderStyle.enabledStyle.markStyle.copyWith( + labelOffset: -20, + labelAnchor: negative, + ), + ), + ]; + }); + + testWidgets('symmetric padding', (tester) async { await tester.pumpWidget( TestScaffold.app( data: FThemes.zinc.light, - // background: background, child: FSlider( controller: FContinuousSliderController( selection: FSliderSelection(min: 0.30, max: 0.60), ), layout: layout, - marks: [ - FSliderMark(value: 0.0, label: const Text('0'), style: style), - FSliderMark(value: 0.25, label: const Text('25'), style: style, tick: false), - FSliderMark(value: 0.5, label: const Text('50'), style: style), - FSliderMark(value: 0.75, label: const Text('75'), style: style, tick: false), - FSliderMark(value: 1.0, label: const Text('100'), style: style), - ], + marks: marks, ), ), ); - final gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); - await gesture.addPointer(location: Offset.zero); - addTearDown(gesture.removePointer); - await tester.pump(); + await expectLater( + find.byType(TestScaffold), + matchesGoldenFile('slider/label-offset/$layout-symmetric.png'), + ); + }); - await gesture.moveTo(tester.getCenter(find.byType(Thumb).first)); - await tester.pumpAndSettle(const Duration(seconds: 1)); + testWidgets('asymmetric cross axis padding - $layout', (tester) async { + await tester.pumpWidget( + TestScaffold.app( + data: FThemes.zinc.light, + child: FSlider( + style: sliderStyle.copyWith( + labelLayoutStyle: sliderStyle.labelLayoutStyle.copyWith( + childPadding: const EdgeInsets.only(left: 20, top: 40, right: 10, bottom: 30), + ), + ), + controller: FContinuousSliderController( + selection: FSliderSelection(min: 0.30, max: 0.60), + ), + layout: layout, + marks: marks, + ), + ), + ); await expectLater( find.byType(TestScaffold), - matchesGoldenFile('slider/label-offset-$layout-$labelOffset.png'), + matchesGoldenFile('slider/label-offset/$layout-asymmetric.png'), ); }); - } + + testWidgets('labelled', (tester) async { + await tester.pumpWidget( + TestScaffold.app( + data: FThemes.zinc.light, + child: FSlider( + label: const Text('Label'), + description: const Text('Description'), + trackMainAxisExtent: 300, + controller: FContinuousSliderController( + selection: FSliderSelection(min: 0.30, max: 0.60), + ), + layout: layout, + marks: marks, + ), + ), + ); + + await expectLater( + find.byType(TestScaffold), + matchesGoldenFile('slider/label-offset/$layout-labelled.png'), + ); + }); + }); } + + testWidgets('interweaving marks with no labels', (tester) async { + await tester.pumpWidget( + TestScaffold.app( + data: FThemes.zinc.light, + child: FSlider( + controller: FContinuousSliderController( + selection: FSliderSelection(min: 0.30, max: 0.60), + ), + marks: const [ + FSliderMark(value: 0, label: Text('0%')), + FSliderMark(value: 0.25, tick: false), + FSliderMark(value: 0.5, label: Text('50%')), + FSliderMark(value: 0.75, tick: false), + FSliderMark(value: 1, label: Text('100%')), + ], + ), + ), + ); + + await expectLater( + find.byType(TestScaffold), + matchesGoldenFile('slider/interweaving-marks.png'), + ); + }); }); + + tearDown(() => Touch.primary = null); } diff --git a/forui/test/src/widgets/slider/slider_test.dart b/forui/test/src/widgets/slider/slider_test.dart index 2e2aa5260..8911b8bab 100644 --- a/forui/test/src/widgets/slider/slider_test.dart +++ b/forui/test/src/widgets/slider/slider_test.dart @@ -9,7 +9,11 @@ import '../../test_scaffold.dart'; void main() { group('value slider tooltip', () { - Widget slider({FSliderSelection? selection, FSliderInteraction? interaction}) => TestScaffold.app( + Widget slider({ + FSliderSelection? selection, + FSliderInteraction interaction = FSliderInteraction.tapAndSlideThumb, + }) => + TestScaffold.app( data: FThemes.zinc.light, child: FSlider( controller: FContinuousSliderController( @@ -52,16 +56,16 @@ void main() { group('drag track', () { for (final (interaction, expected) in [ (FSliderInteraction.slide, findsOneWidget), - (FSliderInteraction.slideThumb, findsNothing), - (FSliderInteraction.tap, findsNothing), - (FSliderInteraction.tapAndSlideThumb, findsNothing), + (FSliderInteraction.slideThumb, findsOneWidget), + (FSliderInteraction.tap, findsOneWidget), + (FSliderInteraction.tapAndSlideThumb, findsOneWidget), ]) { testWidgets('drag active track - $interaction', (tester) async { await tester.pumpWidget(slider(interaction: interaction)); expect(find.byType(Text), findsNothing); await tester.fling(find.byType(ActiveTrack), const Offset(-300, 0), 10); - await tester.pump(const Duration(seconds: 1)); + await tester.pump(); expect(find.byType(Text), expected); }); @@ -161,12 +165,12 @@ void main() { for (final (con, interaction, expandExpected, shrinkExpected) in [ (true, FSliderInteraction.slide, 0.8, 0.5), (true, FSliderInteraction.slideThumb, 0.75, 0.75), - (true, FSliderInteraction.tap, 0.5, 0.5), - (true, FSliderInteraction.tapAndSlideThumb, 0.5, 0.5), + (true, FSliderInteraction.tap, 0.75, 0.75), + (true, FSliderInteraction.tapAndSlideThumb, 0.75, 0.75), (false, FSliderInteraction.slide, 0.75, 0.25), (false, FSliderInteraction.slideThumb, 0.5, 0.5), - (false, FSliderInteraction.tap, 0.25, 0.25), - (false, FSliderInteraction.tapAndSlideThumb, 0.25, 0.25), + (false, FSliderInteraction.tap, 0.5, 0.5), + (false, FSliderInteraction.tapAndSlideThumb, 0.5, 0.5), ]) { testWidgets('drag active track - ${con ? 'continuous' : 'discrete'} - $interaction', (tester) async { final controller = con ? continuous(interaction) : discrete(interaction); @@ -185,14 +189,14 @@ void main() { for (final (con, interaction, expandExpected, shrinkExpected) in [ (true, FSliderInteraction.slide, 0.8, 0.5), (true, FSliderInteraction.slideThumb, 0.75, 0.75), - (true, FSliderInteraction.tap, 0.8, 0.8), - (true, FSliderInteraction.tapAndSlideThumb, 0.8, 0.8), + (true, FSliderInteraction.tap, 0.75, 0.75), + (true, FSliderInteraction.tapAndSlideThumb, 0.75, 0.75), (false, FSliderInteraction.slide, 0.75, 0.25), (false, FSliderInteraction.slideThumb, 0.5, 0.5), (false, FSliderInteraction.tap, 0.5, 0.5), (false, FSliderInteraction.tapAndSlideThumb, 0.5, 0.5), ]) { - testWidgets('drag inactive track - $interaction', (tester) async { + testWidgets('drag inactive track - ${con ? 'continuous' : 'discrete'} - $interaction', (tester) async { final controller = con ? continuous(interaction) : discrete(interaction); await tester.pumpWidget(slider(controller)); diff --git a/forui/test/src/widgets/tooltip/tooltip_golden_test.dart b/forui/test/src/widgets/tooltip/tooltip_golden_test.dart index 0d606c878..db7b9d2b4 100644 --- a/forui/test/src/widgets/tooltip/tooltip_golden_test.dart +++ b/forui/test/src/widgets/tooltip/tooltip_golden_test.dart @@ -31,10 +31,10 @@ void main() { ), ); - await expectLater(find.byType(TestScaffold), matchesGoldenFile('tooltip/$name-hidden.png')); + await expectLater(find.byType(TestScaffold), matchesGoldenFile('tooltip/hidden-$name.png')); }); - testWidgets('$name shown', (tester) async { + testWidgets('$name shown on touch devices', (tester) async { final controller = FTooltipController(vsync: const TestVSync()); await tester.pumpWidget( @@ -61,7 +61,7 @@ void main() { await gesture.moveTo(tester.getCenter(find.byType(ColoredBox).first)); await tester.pumpAndSettle(const Duration(seconds: 5)); - await expectLater(find.byType(TestScaffold), matchesGoldenFile('tooltip/$name-shown.png')); + await expectLater(find.byType(TestScaffold), matchesGoldenFile('tooltip/shown-$name.png')); }); }); } diff --git a/forui/test/src/widgets/tooltip/tooltip_test.dart b/forui/test/src/widgets/tooltip/tooltip_test.dart index 52c9eff32..cb0e0b778 100644 --- a/forui/test/src/widgets/tooltip/tooltip_test.dart +++ b/forui/test/src/widgets/tooltip/tooltip_test.dart @@ -1,4 +1,3 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; @@ -39,8 +38,6 @@ void main() { await tester.pumpAndSettle(const Duration(seconds: 1)); expect(find.text('tip'), findsNothing); - - debugDefaultTargetPlatformOverride = null; }); group('long press', () { diff --git a/samples/lib/main.dart b/samples/lib/main.dart index 383a4cc74..31eda17ba 100644 --- a/samples/lib/main.dart +++ b/samples/lib/main.dart @@ -85,6 +85,8 @@ class _AppRouter extends RootStackRouter { AutoRoute(path: '/slider/marks', page: MarksSliderRoute.page), AutoRoute(path: '/slider/discrete', page: DiscreteSliderRoute.page), AutoRoute(path: '/slider/range', page: RangeSliderRoute.page), + AutoRoute(path: '/slider/vertical', page: VerticalSliderRoute.page), + AutoRoute(path: '/slider/form', page: SliderFormRoute.page), AutoRoute(path: '/switch/default', page: SwitchRoute.page), AutoRoute(path: '/switch/form', page: FormSwitchRoute.page), ]; diff --git a/samples/lib/widgets/slider.dart b/samples/lib/widgets/slider.dart index eb75d4916..55887350f 100644 --- a/samples/lib/widgets/slider.dart +++ b/samples/lib/widgets/slider.dart @@ -7,12 +7,18 @@ import 'package:forui_samples/sample_scaffold.dart'; @RoutePage() class SliderPage extends SampleScaffold { + final String? label; + final String? description; + final String? error; final bool enabled; final FSliderInteraction interaction; final ({double min, double max}) extent; SliderPage({ @queryParam super.theme, + @queryParam this.label, + @queryParam this.description, + @queryParam this.error, @queryParam String enabled = 'true', @queryParam String interaction = 'tapAndSlideThumb', @queryParam String extent = 'false', @@ -23,10 +29,13 @@ class SliderPage extends SampleScaffold { 'tap' => FSliderInteraction.tap, _ => FSliderInteraction.tapAndSlideThumb, }, - extent = bool.tryParse(extent) ?? false ? (min: 0, max: 1) : (min: 0.25, max: 0.75); + extent = bool.tryParse(extent) ?? false ? (min: 0.25, max: 0.75) : (min: 0, max: 1); @override Widget child(BuildContext context) => FSlider( + label: label != null ? Text(label!) : null, + description: description != null ? Text(description!) : null, + forceErrorText: error, controller: FContinuousSliderController( selection: FSliderSelection( max: 0.6, @@ -56,23 +65,14 @@ class TooltipSliderPage extends SampleScaffold { @RoutePage() class MarksSliderPage extends SampleScaffold { - final Layout layout; - MarksSliderPage({ @queryParam super.theme, - @queryParam String layout = 'ltr', - }) : layout = switch (layout) { - 'rtl' => Layout.rtl, - 'ttb' => Layout.ttb, - 'btt' => Layout.btt, - _ => Layout.ltr, - }; + }); @override Widget child(BuildContext context) => Padding( padding: const EdgeInsets.symmetric(vertical: 35), child: FSlider( - layout: layout, controller: FContinuousSliderController(selection: FSliderSelection(max: 0.35)), marks: const [ FSliderMark(value: 0, label: Text('0%')), @@ -115,3 +115,91 @@ class RangeSliderPage extends SampleScaffold { controller: FContinuousSliderController.range(selection: FSliderSelection(min: 0.25, max: 0.75)), ); } + +@RoutePage() +class VerticalSliderPage extends SampleScaffold { + VerticalSliderPage({ + @queryParam super.theme, + }); + + @override + Widget child(BuildContext context) => Padding( + padding: const EdgeInsets.symmetric(vertical: 35), + child: FSlider( + label: const Text('Volume'), + description: const Text('Adjust the volume by dragging the slider.'), + layout: Layout.btt, + controller: FContinuousSliderController(selection: FSliderSelection(max: 0.35)), + trackMainAxisExtent: 350, + marks: const [ + FSliderMark(value: 0, label: Text('0%')), + FSliderMark(value: 0.25, tick: false), + FSliderMark(value: 0.5, label: Text('50%')), + FSliderMark(value: 0.75, tick: false), + FSliderMark(value: 1, label: Text('100%')), + ], + ), + ); +} + +@RoutePage() +class SliderFormPage extends SampleScaffold { + SliderFormPage({ + @queryParam super.theme, + }); + + @override + Widget child(BuildContext context) => ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 300), + child: const SliderForm(), + ); +} + +class SliderForm extends StatefulWidget { + const SliderForm({super.key}); + + @override + State createState() => SliderFormState(); +} + +class SliderFormState extends State { + final GlobalKey _formKey = GlobalKey(); + + @override + Widget build(BuildContext context) => Form( + key: _formKey, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FSlider( + label: const Text('Brightness'), + description: const Text('Adjust the brightness level.'), + controller: FContinuousSliderController( + selection: FSliderSelection(max: 0.35), + ), + marks: const [ + FSliderMark(value: 0, label: Text('0%')), + FSliderMark(value: 0.25, tick: false), + FSliderMark(value: 0.5, label: Text('50%')), + FSliderMark(value: 0.75, tick: false), + FSliderMark(value: 1, label: Text('100%')), + ], + ), + const SizedBox(height: 20), + FButton( + label: const Text('Save'), + onPress: () { + if (!_formKey.currentState!.validate()) { + // Handle errors here. + return; + } + + _formKey.currentState!.save(); + // Do something. + }, + ), + ], + ), + ); +} diff --git a/samples/pubspec.lock b/samples/pubspec.lock index 2ccf1e8d2..24be77b54 100644 --- a/samples/pubspec.lock +++ b/samples/pubspec.lock @@ -272,7 +272,7 @@ packages: path: "../forui" relative: true source: path - version: "0.5.0" + version: "0.5.1" forui_assets: dependency: "direct overridden" description: