diff --git a/packages/mix/lib/mix.dart b/packages/mix/lib/mix.dart index 276a279bc..b33a9e3c5 100644 --- a/packages/mix/lib/mix.dart +++ b/packages/mix/lib/mix.dart @@ -125,4 +125,5 @@ export 'src/variants/context_variant_util/on_util.dart'; export 'src/variants/variant_attribute.dart'; export 'src/variants/widget_state_variant.dart'; /// WIDGETS +export 'src/widgets/gesture_detector_widget.dart'; export 'src/widgets/pressable_widget.dart'; diff --git a/packages/mix/lib/src/widgets/gesture_detector_widget.dart b/packages/mix/lib/src/widgets/gesture_detector_widget.dart new file mode 100644 index 000000000..f4821fcbe --- /dev/null +++ b/packages/mix/lib/src/widgets/gesture_detector_widget.dart @@ -0,0 +1,228 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/widgets.dart'; + +import '../core/widget_state/internal/gesture_mix_state.dart'; +import '../core/widget_state/internal/interactive_mix_state.dart'; +import '../core/widget_state/internal/mix_widget_state_builder.dart'; +import '../core/widget_state/internal/mouse_region_mix_state.dart'; +import '../core/widget_state/widget_state_controller.dart'; +import '../internal/constants.dart'; + +class MixGestureDetector extends StatefulWidget { + const MixGestureDetector({ + super.key, + required this.child, + this.enabled = true, + this.enableFeedback = false, + this.onPress, + this.hitTestBehavior = HitTestBehavior.opaque, + this.onLongPress, + this.onFocusChange, + this.autofocus = false, + this.focusNode, + this.mouseCursor, + this.onKey, + this.canRequestFocus = true, + this.excludeFromSemantics = false, + this.semanticButtonLabel, + this.onKeyEvent, + this.unpressDelay = kDefaultAnimationDuration, + this.controller, + this.actions, + this.onTapUp, + this.onTapCancel, + this.onLongPressStart, + this.onLongPressEnd, + this.onLongPressCancel, + this.onPanDown, + this.onPanUpdate, + this.onPanEnd, + this.onPanCancel, + this.onPanStart, + }); + + final Widget child; + + final bool enabled; + + final MouseCursor? mouseCursor; + + final String? semanticButtonLabel; + + final bool excludeFromSemantics; + + final bool canRequestFocus; + + /// Should gestures provide audible and/or haptic feedback + /// + /// On platforms like Android, enabling feedback will result in audible and tactile + /// responses to certain actions. For example, a tap may produce a clicking sound, + /// while a long-press may trigger a short vibration. + final bool enableFeedback; + + /// The callback that is called when the box is tapped or otherwise activated. + /// + /// If this callback and [onLongPress] are null, then it will be disabled automatically. + final VoidCallback? onPress; + + /// The callback that is called when long-pressed. + /// + /// If this callback and [onPress] are null, then `PressableBox` will be disabled automatically. + final VoidCallback? onLongPress; + + /// Called when a pointer that will trigger a tap has stopped contacting the screen. + final GestureTapUpCallback? onTapUp; + + /// Called when the pointer that previously triggered [onTapDown] will not end up causing a tap. + final GestureTapCancelCallback? onTapCancel; + + /// Called when a long press gesture has been recognized. + final GestureLongPressStartCallback? onLongPressStart; + + /// Called when a long press gesture that had been recognized is ended. + final GestureLongPressEndCallback? onLongPressEnd; + + /// Called when a long press gesture that had been recognized is canceled. + final GestureLongPressCancelCallback? onLongPressCancel; + + /// Called when a pointer has contacted the screen and might begin to move. + final GestureDragDownCallback? onPanDown; + + /// Called when a pointer that is in contact with the screen and moving has moved again. + final GestureDragUpdateCallback? onPanUpdate; + + /// Called when a pointer that was previously in contact with the screen and moving is no longer in contact with the screen. + final GestureDragEndCallback? onPanEnd; + + /// Called when the pointer that previously triggered [onPanDown] did not complete. + final GestureDragCancelCallback? onPanCancel; + + /// Called when a pointer has contacted the screen and has begun to move. + final GestureDragStartCallback? onPanStart; + + /// Called when the focus state of the [Focus] changes. + /// + /// Called with true when the [Focus] node gains focus + /// and false when the [Focus] node loses focus. + final ValueChanged? onFocusChange; + + /// {@macro flutter.widgets.Focus.autofocus} + final bool autofocus; + + /// {@macro flutter.widgets.Focus.focusNode} + final FocusNode? focusNode; + + /// {@macro flutter.widgets.Focus.onKey} + final FocusOnKeyEventCallback? onKey; + + /// {@macro flutter.widgets.Focus.onKeyEvent} + final FocusOnKeyEventCallback? onKeyEvent; + + /// {@macro flutter.widgets.GestureDetector.hitTestBehavior} + final HitTestBehavior hitTestBehavior; + + /// Actions to be bound to the widget + final Map>? actions; + + final MixWidgetStateController? controller; + + /// The duration to wait after the press is released before the state of pressed is removed + final Duration unpressDelay; + + @override + State createState() => MixGestureDetectorState(); +} + +@visibleForTesting +class MixGestureDetectorState extends State { + late final MixWidgetStateController _controller; + + @override + void initState() { + super.initState(); + _controller = widget.controller ?? MixWidgetStateController(); + } + + @override + void dispose() { + if (widget.controller == null) _controller.dispose(); + super.dispose(); + } + + bool get hasOnPress => widget.onPress != null; + + MouseCursor get mouseCursor { + if (widget.mouseCursor != null) { + return widget.mouseCursor!; + } + + if (!widget.enabled) { + return SystemMouseCursors.forbidden; + } + + return hasOnPress ? SystemMouseCursors.click : MouseCursor.defer; + } + + /// Binds the [ActivateIntent] from the Flutter SDK to the onPressed callback by default. + /// This enables SPACE and ENTER key activation on most platforms. + /// Additional actions can be provided externally to extend functionality. + Map> get actions { + return { + ActivateIntent: + CallbackAction(onInvoke: (_) => widget.onPress?.call()), + ...?widget.actions, + }; + } + + @override + Widget build(BuildContext context) { + Widget current = GestureMixStateWidget( + enableFeedback: widget.enableFeedback, + controller: _controller, + onTap: widget.enabled ? widget.onPress : null, + onLongPress: widget.enabled ? widget.onLongPress : null, + onTapUp: widget.enabled ? widget.onTapUp : null, + onTapCancel: widget.enabled ? widget.onTapCancel : null, + onLongPressStart: widget.enabled ? widget.onLongPressStart : null, + onLongPressEnd: widget.enabled ? widget.onLongPressEnd : null, + onLongPressCancel: widget.enabled ? widget.onLongPressCancel : null, + onPanDown: widget.enabled ? widget.onPanDown : null, + onPanUpdate: widget.enabled ? widget.onPanUpdate : null, + onPanEnd: widget.enabled ? widget.onPanEnd : null, + onPanCancel: widget.enabled ? widget.onPanCancel : null, + onPanStart: widget.enabled ? widget.onPanStart : null, + excludeFromSemantics: widget.excludeFromSemantics, + hitTestBehavior: widget.hitTestBehavior, + unpressDelay: widget.unpressDelay, + child: InteractiveMixStateWidget( + enabled: widget.enabled, + onFocusChange: widget.onFocusChange, + autofocus: widget.autofocus, + focusNode: widget.focusNode, + onKey: widget.onKey, + onKeyEvent: widget.onKeyEvent, + canRequestFocus: widget.canRequestFocus, + mouseCursor: mouseCursor, + controller: _controller, + actions: actions, + child: MouseRegionMixStateWidget( + child: MixWidgetStateBuilder( + controller: _controller, + builder: (_) => widget.child, + ), + ), + ), + ); + + if (!widget.excludeFromSemantics) { + current = Semantics( + button: true, + label: widget.semanticButtonLabel, + onTap: widget.onPress, + child: current, + ); + } + + return current; + } +} diff --git a/packages/mix/lib/src/widgets/pressable_widget.dart b/packages/mix/lib/src/widgets/pressable_widget.dart index 6f38d7363..76324bcee 100644 --- a/packages/mix/lib/src/widgets/pressable_widget.dart +++ b/packages/mix/lib/src/widgets/pressable_widget.dart @@ -1,13 +1,13 @@ import 'package:flutter/material.dart'; import '../core/factory/style_mix.dart'; -import '../core/widget_state/internal/gesture_mix_state.dart'; import '../core/widget_state/internal/interactive_mix_state.dart'; import '../core/widget_state/internal/mix_widget_state_builder.dart'; import '../core/widget_state/internal/mouse_region_mix_state.dart'; import '../core/widget_state/widget_state_controller.dart'; import '../internal/constants.dart'; import '../specs/box/box_widget.dart'; +import 'gesture_detector_widget.dart'; class PressableBox extends StatelessWidget { const PressableBox({ @@ -68,7 +68,7 @@ class PressableBox extends StatelessWidget { } } -class Pressable extends StatefulWidget { +class Pressable extends StatelessWidget { const Pressable({ super.key, required this.child, @@ -89,9 +89,6 @@ class Pressable extends StatefulWidget { this.unpressDelay = kDefaultAnimationDuration, this.controller, this.actions, - this.onPanDown, - this.onPanUpdate, - this.onPanEnd, }); final Widget child; @@ -106,10 +103,6 @@ class Pressable extends StatefulWidget { final bool canRequestFocus; - final void Function(DragDownDetails)? onPanDown; - final void Function(DragUpdateDetails)? onPanUpdate; - final void Function(DragEndDetails)? onPanEnd; - /// Should gestures provide audible and/or haptic feedback /// /// On platforms like Android, enabling feedback will result in audible and tactile @@ -156,94 +149,28 @@ class Pressable extends StatefulWidget { /// The duration to wait after the press is released before the state of pressed is removed final Duration unpressDelay; - @override - State createState() => PressableWidgetState(); -} - -@visibleForTesting -class PressableWidgetState extends State { - late final MixWidgetStateController _controller; - - @override - void initState() { - super.initState(); - _controller = widget.controller ?? MixWidgetStateController(); - } - - @override - void dispose() { - if (widget.controller == null) _controller.dispose(); - super.dispose(); - } - - bool get hasOnPress => widget.onPress != null; - - MouseCursor get mouseCursor { - if (widget.mouseCursor != null) { - return widget.mouseCursor!; - } - - if (!widget.enabled) { - return SystemMouseCursors.forbidden; - } - - return hasOnPress ? SystemMouseCursors.click : MouseCursor.defer; - } - - /// Binds the [ActivateIntent] from the Flutter SDK to the onPressed callback by default. - /// This enables SPACE and ENTER key activation on most platforms. - /// Additional actions can be provided externally to extend functionality. - Map> get actions { - return { - ActivateIntent: - CallbackAction(onInvoke: (_) => widget.onPress?.call()), - ...?widget.actions, - }; - } - @override Widget build(BuildContext context) { - Widget current = GestureMixStateWidget( - enableFeedback: widget.enableFeedback, - controller: _controller, - onTap: widget.enabled ? widget.onPress?.call : null, - onLongPress: widget.enabled ? widget.onLongPress?.call : null, - onPanDown: widget.enabled ? widget.onPanDown?.call : null, - onPanUpdate: widget.enabled ? widget.onPanUpdate?.call : null, - onPanEnd: widget.enabled ? widget.onPanEnd?.call : null, - excludeFromSemantics: widget.excludeFromSemantics, - hitTestBehavior: widget.hitTestBehavior, - unpressDelay: widget.unpressDelay, - child: InteractiveMixStateWidget( - enabled: widget.enabled, - onFocusChange: widget.onFocusChange, - autofocus: widget.autofocus, - focusNode: widget.focusNode, - onKey: widget.onKey, - onKeyEvent: widget.onKeyEvent, - canRequestFocus: widget.canRequestFocus, - mouseCursor: mouseCursor, - controller: _controller, - actions: actions, - child: MouseRegionMixStateWidget( - child: MixWidgetStateBuilder( - controller: _controller, - builder: (_) => widget.child, - ), - ), - ), + return MixGestureDetector( + enabled: enabled, + enableFeedback: enableFeedback, + onPress: onPress, + hitTestBehavior: hitTestBehavior, + onLongPress: onLongPress, + onFocusChange: onFocusChange, + autofocus: autofocus, + focusNode: focusNode, + mouseCursor: mouseCursor, + onKey: onKey, + canRequestFocus: canRequestFocus, + excludeFromSemantics: excludeFromSemantics, + semanticButtonLabel: semanticButtonLabel, + onKeyEvent: onKeyEvent, + unpressDelay: unpressDelay, + controller: controller, + actions: actions, + child: child, ); - - if (!widget.excludeFromSemantics) { - current = Semantics( - button: true, - label: widget.semanticButtonLabel, - onTap: widget.onPress, - child: current, - ); - } - - return current; } } diff --git a/packages/mix/test/src/widgets/pressable/pressable_widget_test.dart b/packages/mix/test/src/widgets/pressable/pressable_widget_test.dart index 7fd5fb8d0..0684a213c 100644 --- a/packages/mix/test/src/widgets/pressable/pressable_widget_test.dart +++ b/packages/mix/test/src/widgets/pressable/pressable_widget_test.dart @@ -560,7 +560,7 @@ void main() { final finder = find.byType(Pressable); expect(finder, findsOneWidget); - final pressableState = tester.state(finder); + final pressableState = tester.state(finder); expect(pressableState.mouseCursor, equals(SystemMouseCursors.help)); }); @@ -576,7 +576,7 @@ void main() { final finder = find.byType(Pressable); expect(finder, findsOneWidget); - final pressableState = tester.state(finder); + final pressableState = tester.state(finder); expect(pressableState.mouseCursor, equals(SystemMouseCursors.forbidden)); }); @@ -592,7 +592,7 @@ void main() { final finder = find.byType(Pressable); expect(finder, findsOneWidget); - final pressableState = tester.state(finder); + final pressableState = tester.state(finder); expect(pressableState.mouseCursor, equals(SystemMouseCursors.click)); }); @@ -607,7 +607,7 @@ void main() { final finder = find.byType(Pressable); expect(finder, findsOneWidget); - final pressableState = tester.state(finder); + final pressableState = tester.state(finder); expect(pressableState.mouseCursor, equals(MouseCursor.defer)); }); }); diff --git a/packages/remix/demo/lib/components/slider.dart b/packages/remix/demo/lib/components/slider.dart new file mode 100644 index 000000000..ff40d346c --- /dev/null +++ b/packages/remix/demo/lib/components/slider.dart @@ -0,0 +1,42 @@ +import 'package:demo/helpers/use_case_state.dart'; +import 'package:flutter/widgets.dart'; +import 'package:remix/remix.dart'; +import 'package:widgetbook/widgetbook.dart'; +import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; + +final _key = GlobalKey(); + +@widgetbook.UseCase( + name: 'Slider Component', + type: Slider, +) +Widget buildButtonUseCase(BuildContext context) { + final knobState = WidgetbookState.of(context); + return KeyedSubtree( + key: _key, + child: Scaffold( + body: Center( + child: SizedBox( + width: 200, + child: Slider( + onChanged: (value) => knobState.updateKnob('value', value), + disabled: context.knobs.boolean( + label: 'disabled', + initialValue: false, + ), + divisions: context.knobs.int.input( + label: 'divisions', + initialValue: 0, + ), + value: context.knobs.double.slider( + label: 'value', + min: 0, + max: 1, + initialValue: 0.25, + ), + ), + ), + ), + ), + ); +} diff --git a/packages/remix/demo/lib/main.directories.g.dart b/packages/remix/demo/lib/main.directories.g.dart index 1db34b2b3..8af1f5ce9 100644 --- a/packages/remix/demo/lib/main.directories.g.dart +++ b/packages/remix/demo/lib/main.directories.g.dart @@ -24,9 +24,10 @@ import 'package:demo/components/progress_use_case.dart' as _i13; import 'package:demo/components/radio_use_case.dart' as _i14; import 'package:demo/components/segmented_control_use_case.dart' as _i15; import 'package:demo/components/select_use_case.dart' as _i16; -import 'package:demo/components/spinner_use_case.dart' as _i17; -import 'package:demo/components/switch_use_case.dart' as _i18; -import 'package:demo/components/toast_use_case.dart' as _i19; +import 'package:demo/components/slider.dart' as _i17; +import 'package:demo/components/spinner_use_case.dart' as _i18; +import 'package:demo/components/switch_use_case.dart' as _i19; +import 'package:demo/components/toast_use_case.dart' as _i20; import 'package:widgetbook/widgetbook.dart' as _i1; final directories = <_i1.WidgetbookNode>[ @@ -213,6 +214,18 @@ final directories = <_i1.WidgetbookNode>[ ) ], ), + _i1.WidgetbookFolder( + name: 'slider', + children: [ + _i1.WidgetbookLeafComponent( + name: 'Slider', + useCase: _i1.WidgetbookUseCase( + name: 'Slider Component', + builder: _i17.buildButtonUseCase, + ), + ) + ], + ), _i1.WidgetbookFolder( name: 'spinner', children: [ @@ -220,7 +233,7 @@ final directories = <_i1.WidgetbookNode>[ name: 'Spinner', useCase: _i1.WidgetbookUseCase( name: 'Spinner Component', - builder: _i17.buildSpinnerUseCase, + builder: _i18.buildSpinnerUseCase, ), ) ], @@ -232,7 +245,7 @@ final directories = <_i1.WidgetbookNode>[ name: 'Switch', useCase: _i1.WidgetbookUseCase( name: 'Switch Component', - builder: _i18.buildSwitchUseCase, + builder: _i19.buildSwitchUseCase, ), ) ], @@ -244,7 +257,7 @@ final directories = <_i1.WidgetbookNode>[ name: 'Toast', useCase: _i1.WidgetbookUseCase( name: 'Toast Component', - builder: _i19.buildButtonUseCase, + builder: _i20.buildButtonUseCase, ), ) ], diff --git a/packages/remix/lib/remix.dart b/packages/remix/lib/remix.dart index 8c30dc0db..89ccbfcd3 100644 --- a/packages/remix/lib/remix.dart +++ b/packages/remix/lib/remix.dart @@ -37,6 +37,7 @@ export 'src/components/progress/progress.dart'; export 'src/components/radio/radio.dart'; export 'src/components/segmented_control/segmented_control.dart'; export 'src/components/select/select.dart'; +export 'src/components/slider/slider.dart'; export 'src/components/spinner/spinner.dart'; export 'src/components/switch/switch.dart'; export 'src/components/toast/toast.dart'; diff --git a/packages/remix/lib/src/components/slider/slider.dart b/packages/remix/lib/src/components/slider/slider.dart index c03fd141e..c5ae35565 100644 --- a/packages/remix/lib/src/components/slider/slider.dart +++ b/packages/remix/lib/src/components/slider/slider.dart @@ -4,10 +4,11 @@ import 'package:mix/mix.dart'; import 'package:mix_annotations/mix_annotations.dart'; import '../../theme/remix_theme.dart'; +import '../../theme/remix_tokens.dart'; part 'slider.g.dart'; part 'slider_style.dart'; -// part 'slider_theme.dart'; +part 'slider_theme.dart'; part 'slider_widget.dart'; @MixableSpec() @@ -16,7 +17,6 @@ class SliderSpec extends Spec with _$SliderSpec, Diagnosticable { final BoxSpec track; final BoxSpec activeTrack; final BoxSpec division; - final EdgeInsetsGeometry padding; /// {@macro button_spec_of} static const of = _$SliderSpec.of; @@ -27,14 +27,12 @@ class SliderSpec extends Spec with _$SliderSpec, Diagnosticable { BoxSpec? thumb, BoxSpec? track, BoxSpec? activeTrack, - EdgeInsetsGeometry? padding, BoxSpec? division, super.modifiers, super.animated, }) : thumb = thumb ?? const BoxSpec(), track = track ?? const BoxSpec(), activeTrack = activeTrack ?? const BoxSpec(), - padding = padding ?? EdgeInsets.zero, division = division ?? const BoxSpec(); @override diff --git a/packages/remix/lib/src/components/slider/slider.g.dart b/packages/remix/lib/src/components/slider/slider.g.dart index 27525c3fe..3dac4d586 100644 --- a/packages/remix/lib/src/components/slider/slider.g.dart +++ b/packages/remix/lib/src/components/slider/slider.g.dart @@ -35,7 +35,6 @@ mixin _$SliderSpec on Spec { BoxSpec? thumb, BoxSpec? track, BoxSpec? activeTrack, - EdgeInsetsGeometry? padding, BoxSpec? division, WidgetModifiersData? modifiers, AnimatedData? animated, @@ -44,7 +43,6 @@ mixin _$SliderSpec on Spec { thumb: thumb ?? _$this.thumb, track: track ?? _$this.track, activeTrack: activeTrack ?? _$this.activeTrack, - padding: padding ?? _$this.padding, division: division ?? _$this.division, modifiers: modifiers ?? _$this.modifiers, animated: animated ?? _$this.animated, @@ -63,7 +61,6 @@ mixin _$SliderSpec on Spec { /// interpolation method: /// /// - [BoxSpec.lerp] for [thumb] and [track] and [activeTrack] and [division]. - /// - [EdgeInsetsGeometry.lerp] for [padding]. /// For [modifiers] and [animated], the interpolation is performed using a step function. /// If [t] is less than 0.5, the value from the current [SliderSpec] is used. Otherwise, the value @@ -79,7 +76,6 @@ mixin _$SliderSpec on Spec { thumb: _$this.thumb.lerp(other.thumb, t), track: _$this.track.lerp(other.track, t), activeTrack: _$this.activeTrack.lerp(other.activeTrack, t), - padding: EdgeInsetsGeometry.lerp(_$this.padding, other.padding, t)!, division: _$this.division.lerp(other.division, t), modifiers: other.modifiers, animated: t < 0.5 ? _$this.animated : other.animated, @@ -95,7 +91,6 @@ mixin _$SliderSpec on Spec { _$this.thumb, _$this.track, _$this.activeTrack, - _$this.padding, _$this.division, _$this.modifiers, _$this.animated, @@ -110,8 +105,6 @@ mixin _$SliderSpec on Spec { .add(DiagnosticsProperty('track', _$this.track, defaultValue: null)); properties.add(DiagnosticsProperty('activeTrack', _$this.activeTrack, defaultValue: null)); - properties.add( - DiagnosticsProperty('padding', _$this.padding, defaultValue: null)); properties.add( DiagnosticsProperty('division', _$this.division, defaultValue: null)); properties.add( @@ -133,14 +126,12 @@ class SliderSpecAttribute extends SpecAttribute final BoxSpecAttribute? thumb; final BoxSpecAttribute? track; final BoxSpecAttribute? activeTrack; - final SpacingDto? padding; final BoxSpecAttribute? division; const SliderSpecAttribute({ this.thumb, this.track, this.activeTrack, - this.padding, this.division, super.modifiers, super.animated, @@ -160,7 +151,6 @@ class SliderSpecAttribute extends SpecAttribute thumb: thumb?.resolve(mix), track: track?.resolve(mix), activeTrack: activeTrack?.resolve(mix), - padding: padding?.resolve(mix), division: division?.resolve(mix), modifiers: modifiers?.resolve(mix), animated: animated?.resolve(mix) ?? mix.animation, @@ -183,7 +173,6 @@ class SliderSpecAttribute extends SpecAttribute thumb: thumb?.merge(other.thumb) ?? other.thumb, track: track?.merge(other.track) ?? other.track, activeTrack: activeTrack?.merge(other.activeTrack) ?? other.activeTrack, - padding: padding?.merge(other.padding) ?? other.padding, division: division?.merge(other.division) ?? other.division, modifiers: modifiers?.merge(other.modifiers) ?? other.modifiers, animated: animated?.merge(other.animated) ?? other.animated, @@ -199,7 +188,6 @@ class SliderSpecAttribute extends SpecAttribute thumb, track, activeTrack, - padding, division, modifiers, animated, @@ -212,7 +200,6 @@ class SliderSpecAttribute extends SpecAttribute properties.add(DiagnosticsProperty('track', track, defaultValue: null)); properties.add( DiagnosticsProperty('activeTrack', activeTrack, defaultValue: null)); - properties.add(DiagnosticsProperty('padding', padding, defaultValue: null)); properties .add(DiagnosticsProperty('division', division, defaultValue: null)); properties @@ -237,9 +224,6 @@ class SliderSpecUtility /// Utility for defining [SliderSpecAttribute.activeTrack] late final activeTrack = BoxSpecUtility((v) => only(activeTrack: v)); - /// Utility for defining [SliderSpecAttribute.padding] - late final padding = SpacingUtility((v) => only(padding: v)); - /// Utility for defining [SliderSpecAttribute.division] late final division = BoxSpecUtility((v) => only(division: v)); @@ -263,7 +247,6 @@ class SliderSpecUtility BoxSpecAttribute? thumb, BoxSpecAttribute? track, BoxSpecAttribute? activeTrack, - SpacingDto? padding, BoxSpecAttribute? division, WidgetModifiersDataDto? modifiers, AnimatedDataDto? animated, @@ -272,7 +255,6 @@ class SliderSpecUtility thumb: thumb, track: track, activeTrack: activeTrack, - padding: padding, division: division, modifiers: modifiers, animated: animated, diff --git a/packages/remix/lib/src/components/slider/slider_style.dart b/packages/remix/lib/src/components/slider/slider_style.dart index 6bb771e61..084650191 100644 --- a/packages/remix/lib/src/components/slider/slider_style.dart +++ b/packages/remix/lib/src/components/slider/slider_style.dart @@ -7,39 +7,39 @@ class SliderStyle extends SpecStyle { Style makeStyle(SpecConfiguration spec) { final $ = spec.utilities; - return Style.create([ - // $.divisionRadius(1.5), - // $.divisionColor.black.brighten(70), - $.division.shape.circle(), - $.division.color.black26(), - $.division.height(3), - $.division.width(3), - - $.padding.horizontal(24), - $.thumb.color.white.withOpacity(1), - $.thumb.shape.circle(), - $.track.color.white.withLightness(0.9), - $.activeTrack.color.white.withLightness(0.1), - $.thumb.chain - ..shape.circle.side.width(2) - ..shape.circle.side.color.black() - ..shape.circle.side.style.solid() - ..shape.circle.side.strokeAlign(BorderSide.strokeAlignInside) - ..height(20) - ..width(20), - $.track.height(6), - $.track.borderRadius.all(10), - $.activeTrack.height(6), - $.activeTrack.borderRadius.all(10), - spec.on.disabled($.activeTrack.color.white.withLightness(0.7)), - (spec.on.hover | spec.on.dragged)( - $.thumb.chain - ..shadow.blurRadius(0) - ..shadow.color.black.withOpacity(0.05) - ..shadow.offset(0, 0) - ..shadow.spreadRadius(8), - ), - ]).animate( + final divisions = $.division.chain + ..shape.circle() + ..color.black26() + ..height(3) + ..width(3); + + final thumb = $.thumb.chain + ..shape.circle() + ..color.white.withOpacity(1) + ..height(20) + ..width(20) + ..shape.circle.side.width(2) + ..shape.circle.side.color.black() + ..shape.circle.side.style.solid() + ..shape.circle.side.strokeAlign(BorderSide.strokeAlignInside); + + final track = $.track.chain + ..color.white.withLightness(0.9) + ..height(6) + ..borderRadius.all(10); + + final activeTrack = $.activeTrack.chain + ..color.white.withLightness(0.1) + ..height(6) + ..borderRadius.all(10); + + final disabled = spec.on.disabled( + $.activeTrack.color.white.withLightness(0.7), + $.thumb.shape.circle.side.color.black.withOpacity(0.5), + ); + + return Style.create([divisions, thumb, track, activeTrack, disabled]) + .animate( duration: const Duration(milliseconds: 150), curve: Curves.easeInOut, ); diff --git a/packages/remix/lib/src/components/slider/slider_theme.dart b/packages/remix/lib/src/components/slider/slider_theme.dart new file mode 100644 index 000000000..9593cc547 --- /dev/null +++ b/packages/remix/lib/src/components/slider/slider_theme.dart @@ -0,0 +1,52 @@ +part of 'slider.dart'; + +class FortalezaSliderStyle extends SliderStyle { + const FortalezaSliderStyle(); + + @override + Style makeStyle(SpecConfiguration spec) { + final $ = spec.utilities; + + final baseStyle = super.makeStyle(spec); + final divisions = $.division.color.$neutralAlpha(8); + + final thumb = $.thumb.chain + ..color.$neutral(1) + ..shape.circle.side.color.$accent(9); + + final track = $.track.chain + ..color.resetDirectives() + ..color.$accent(4) + ..height(6) + ..borderRadius.all(10); + + final activeTrack = $.activeTrack.chain + ..color.resetDirectives() + ..color.$accent(9) + ..height(6) + ..borderRadius.all(10); + + final disabled = spec.on.disabled( + $.activeTrack.color.white.withLightness(0.7), + $.thumb.shape.circle.side.color.black.withOpacity(0.5), + ); + + final animation = (spec.on.hover | spec.on.dragged)( + $.thumb.shadow.spreadRadius(8), + $.thumb.shadow.color.$accentAlpha(4), + ); + + return Style.create([ + baseStyle(), + divisions, + thumb, + track, + activeTrack, + disabled, + animation, + ]).animate( + duration: const Duration(milliseconds: 100), + curve: Curves.easeInOut, + ); + } +} diff --git a/packages/remix/lib/src/components/slider/slider_widget.dart b/packages/remix/lib/src/components/slider/slider_widget.dart index 3c66ef49f..2108b9a36 100644 --- a/packages/remix/lib/src/components/slider/slider_widget.dart +++ b/packages/remix/lib/src/components/slider/slider_widget.dart @@ -4,7 +4,7 @@ class Slider extends StatefulWidget { const Slider({ super.key, this.min = 0.0, - this.max = 100.0, + this.max = 1.0, this.divisions = 0, required this.onChanged, required this.value, @@ -56,7 +56,7 @@ class _SliderState extends State with TickerProviderStateMixin { final style = widget.style ?? context.remix.components.slider; final configuration = SpecConfiguration(context, SliderSpecUtility.self); - return Pressable( + return MixGestureDetector( enabled: !widget.disabled, onPanUpdate: (details) { final value = _calculateValue(details.localPosition); @@ -66,6 +66,10 @@ class _SliderState extends State with TickerProviderStateMixin { final value = _calculateValue(details.localPosition); widget.onChangeEnd?.call(value); }, + onPanStart: (details) { + final value = _calculateValue(details.localPosition); + widget.onChangeStart?.call(value); + }, child: SpecBuilder( style: style.makeStyle(configuration).applyVariants(widget.variants), builder: (context) { @@ -83,16 +87,12 @@ class _SliderState extends State with TickerProviderStateMixin { ((widget.value - widget.min) / (widget.max - widget.min)) .clamp(0, 1); + final a = + (constraints.maxWidth - _thumbWidth / 4) / widget.divisions; final divisions = [ for (int i = 0; i < widget.divisions; i++) Transform.translate( - offset: Offset( - (constraints.maxWidth - - _thumbWidth / widget.divisions) / - widget.divisions * - (i + 1), - 0, - ), + offset: Offset(a * (i + 1), 0), child: spec.division(), ), ]; diff --git a/packages/remix/lib/src/theme/remix_theme.dart b/packages/remix/lib/src/theme/remix_theme.dart index 9b4e88989..f27a061de 100644 --- a/packages/remix/lib/src/theme/remix_theme.dart +++ b/packages/remix/lib/src/theme/remix_theme.dart @@ -110,7 +110,7 @@ class RemixComponentTheme { spinner: FortalezaSpinnerStyle(), switchComponent: FortalezaSwitchStyle(), toast: FortalezaToastStyle(), - slider: SliderStyle(), + slider: FortalezaSliderStyle(), ); }