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: