diff --git a/packages/mix/lib/mix.dart b/packages/mix/lib/mix.dart index 9c6391ab0..1fda7fcbb 100644 --- a/packages/mix/lib/mix.dart +++ b/packages/mix/lib/mix.dart @@ -1,15 +1,15 @@ -/// /\\\\ /\\\\ /\\\\\\\\\\\ /\\\ /\\\ -/// \/\\\\\\ /\\\\\\ \/////\\\/// \///\\\ /\\\/ -/// \/\\\//\\\ /\\\//\\\ \/\\\ \///\\\\\\/ -/// \/\\\\///\\\/\\\/ \/\\\ \/\\\ \//\\\\ -/// \/\\\ \///\\\/ \/\\\ \/\\\ \/\\\\ -/// \/\\\ \/// \/\\\ \/\\\ /\\\\\\ -/// \/\\\ \/\\\ \/\\\ /\\\////\\\ -/// \/\\\ \/\\\ /\\\\\\\\\\\ /\\\/ \///\\\ -/// \/// \/// \/////////// \/// \/// +/// /\\\\ /\\\\ /\\\\\\\\\\\ /\\\ /\\\ +/// \/\\\\\\ /\\\\\\ \/////\\\/// \///\\\ /\\\/ +/// \/\\\//\\\ /\\\//\\\ \/\\\ \///\\\\\\/ +/// \/\\\\///\\\/\\\/ \/\\\ \/\\\ \//\\\\ +/// \/\\\ \///\\\/ \/\\\ \/\\\ \/\\\\ +/// \/\\\ \/// \/\\\ \/\\\ /\\\\\\ +/// \/\\\ \/\\\ \/\\\ /\\\////\\\ +/// \/\\\ \/\\\ /\\\\\\\\\\\ /\\\/ \///\\\ +/// \/// \/// \/////////// \/// \/// +/// +/// https://fluttermix.com /// -/// https://fluttermix.com -/// /// /\///////////////////////////////////////////////////\ /// \/\ ***** GENERATED CODE ***** \ \ /// \/\ ** DO NOT EDIT THIS FILE ** \ \ @@ -54,6 +54,7 @@ export 'src/attributes/strut_style/strut_style_dto.dart'; export 'src/attributes/text_height_behavior/text_height_behavior_dto.dart'; export 'src/attributes/text_style/text_style_dto.dart'; export 'src/attributes/text_style/text_style_util.dart'; + /// CORE export 'src/core/attribute.dart'; export 'src/core/attributes_map.dart'; @@ -71,6 +72,7 @@ export 'src/core/styled_widget.dart'; export 'src/core/utility.dart'; export 'src/core/variant.dart'; export 'src/core/widget_state/widget_state_controller.dart'; + /// MODIFIERS export 'src/modifiers/align_widget_modifier.dart'; export 'src/modifiers/aspect_ratio_widget_modifier.dart'; @@ -87,6 +89,7 @@ export 'src/modifiers/sized_box_widget_modifier.dart'; export 'src/modifiers/transform_widget_modifier.dart'; export 'src/modifiers/visibility_widget_modifier.dart'; export 'src/modifiers/widget_modifiers_util.dart'; + /// SPECS export 'src/specs/box/box_spec.dart'; export 'src/specs/box/box_widget.dart'; @@ -102,6 +105,7 @@ export 'src/specs/stack/stack_widget.dart'; export 'src/specs/text/text_directives_util.dart'; export 'src/specs/text/text_spec.dart'; export 'src/specs/text/text_widget.dart'; + /// THEME export 'src/theme/material/material_theme.dart'; export 'src/theme/material/material_tokens.dart'; @@ -114,6 +118,7 @@ export 'src/theme/tokens/space_token.dart'; export 'src/theme/tokens/text_style_token.dart'; export 'src/theme/tokens/token_resolver.dart'; export 'src/theme/tokens/token_util.dart'; + /// VARIANTS export 'src/variants/context_variant.dart'; export 'src/variants/context_variant_util/on_breakpoint_util.dart'; @@ -125,5 +130,6 @@ export 'src/variants/context_variant_util/on_platform_util.dart'; 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/pressable_widget.dart'; 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 ef4171f99..f1518b42b 100644 --- a/packages/remix/demo/lib/main.directories.g.dart +++ b/packages/remix/demo/lib/main.directories.g.dart @@ -25,9 +25,10 @@ import 'package:demo/components/progress_use_case.dart' as _i14; import 'package:demo/components/radio_use_case.dart' as _i15; import 'package:demo/components/segmented_control_use_case.dart' as _i16; import 'package:demo/components/select_use_case.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:demo/components/slider.dart' as _i18; +import 'package:demo/components/spinner_use_case.dart' as _i19; +import 'package:demo/components/switch_use_case.dart' as _i20; +import 'package:demo/components/toast_use_case.dart' as _i21; import 'package:widgetbook/widgetbook.dart' as _i1; final directories = <_i1.WidgetbookNode>[ @@ -226,6 +227,18 @@ final directories = <_i1.WidgetbookNode>[ ) ], ), + _i1.WidgetbookFolder( + name: 'slider', + children: [ + _i1.WidgetbookLeafComponent( + name: 'Slider', + useCase: _i1.WidgetbookUseCase( + name: 'Slider Component', + builder: _i18.buildButtonUseCase, + ), + ) + ], + ), _i1.WidgetbookFolder( name: 'spinner', children: [ @@ -233,7 +246,7 @@ final directories = <_i1.WidgetbookNode>[ name: 'Spinner', useCase: _i1.WidgetbookUseCase( name: 'Spinner Component', - builder: _i18.buildSpinnerUseCase, + builder: _i19.buildSpinnerUseCase, ), ) ], @@ -245,7 +258,7 @@ final directories = <_i1.WidgetbookNode>[ name: 'Switch', useCase: _i1.WidgetbookUseCase( name: 'Switch Component', - builder: _i19.buildSwitchUseCase, + builder: _i20.buildSwitchUseCase, ), ) ], @@ -257,7 +270,7 @@ final directories = <_i1.WidgetbookNode>[ name: 'Toast', useCase: _i1.WidgetbookUseCase( name: 'Toast Component', - builder: _i20.buildButtonUseCase, + builder: _i21.buildButtonUseCase, ), ) ], diff --git a/packages/remix/lib/remix.dart b/packages/remix/lib/remix.dart index 148dee049..c7287df13 100644 --- a/packages/remix/lib/remix.dart +++ b/packages/remix/lib/remix.dart @@ -39,6 +39,7 @@ export 'src/components/radio/radio.dart'; export 'src/components/scaffold/scaffold.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/chip/chip_style.dart b/packages/remix/lib/src/components/chip/chip_style.dart index 2b33f4832..def6f6fca 100644 --- a/packages/remix/lib/src/components/chip/chip_style.dart +++ b/packages/remix/lib/src/components/chip/chip_style.dart @@ -23,12 +23,8 @@ class ChipStyle extends SpecStyle { final labelStyle = [ $.label.chain - ..textHeightBehavior( - const TextHeightBehavior( - applyHeightToFirstAscent: false, - applyHeightToLastDescent: true, - ), - ) + ..textHeightBehavior.heightToFirstAscent.off() + ..textHeightBehavior.heightToLastDescent.on() ..style.fontSize(14) ..style.height(1.5) ..style.color.black() diff --git a/packages/remix/lib/src/components/slider/slider.dart b/packages/remix/lib/src/components/slider/slider.dart new file mode 100644 index 000000000..c5ae35565 --- /dev/null +++ b/packages/remix/lib/src/components/slider/slider.dart @@ -0,0 +1,43 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +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_widget.dart'; + +@MixableSpec() +class SliderSpec extends Spec with _$SliderSpec, Diagnosticable { + final BoxSpec thumb; + final BoxSpec track; + final BoxSpec activeTrack; + final BoxSpec division; + + /// {@macro button_spec_of} + static const of = _$SliderSpec.of; + + static const from = _$SliderSpec.from; + + SliderSpec({ + BoxSpec? thumb, + BoxSpec? track, + BoxSpec? activeTrack, + BoxSpec? division, + super.modifiers, + super.animated, + }) : thumb = thumb ?? const BoxSpec(), + track = track ?? const BoxSpec(), + activeTrack = activeTrack ?? const BoxSpec(), + division = division ?? const BoxSpec(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + _debugFillProperties(properties); + } +} diff --git a/packages/remix/lib/src/components/slider/slider.g.dart b/packages/remix/lib/src/components/slider/slider.g.dart new file mode 100644 index 000000000..3dac4d586 --- /dev/null +++ b/packages/remix/lib/src/components/slider/slider.g.dart @@ -0,0 +1,287 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'slider.dart'; + +// ************************************************************************** +// MixableSpecGenerator +// ************************************************************************** + +mixin _$SliderSpec on Spec { + static SliderSpec from(MixData mix) { + return mix.attributeOf()?.resolve(mix) ?? SliderSpec(); + } + + /// {@template slider_spec_of} + /// Retrieves the [SliderSpec] from the nearest [Mix] ancestor in the widget tree. + /// + /// This method uses [Mix.of] to obtain the [Mix] instance associated with the + /// given [BuildContext], and then retrieves the [SliderSpec] from that [Mix]. + /// If no ancestor [Mix] is found, this method returns an empty [SliderSpec]. + /// + /// Example: + /// + /// ```dart + /// final sliderSpec = SliderSpec.of(context); + /// ``` + /// {@endtemplate} + static SliderSpec of(BuildContext context) { + return _$SliderSpec.from(Mix.of(context)); + } + + /// Creates a copy of this [SliderSpec] but with the given fields + /// replaced with the new values. + @override + SliderSpec copyWith({ + BoxSpec? thumb, + BoxSpec? track, + BoxSpec? activeTrack, + BoxSpec? division, + WidgetModifiersData? modifiers, + AnimatedData? animated, + }) { + return SliderSpec( + thumb: thumb ?? _$this.thumb, + track: track ?? _$this.track, + activeTrack: activeTrack ?? _$this.activeTrack, + division: division ?? _$this.division, + modifiers: modifiers ?? _$this.modifiers, + animated: animated ?? _$this.animated, + ); + } + + /// Linearly interpolates between this [SliderSpec] and another [SliderSpec] based on the given parameter [t]. + /// + /// The parameter [t] represents the interpolation factor, typically ranging from 0.0 to 1.0. + /// When [t] is 0.0, the current [SliderSpec] is returned. When [t] is 1.0, the [other] [SliderSpec] is returned. + /// For values of [t] between 0.0 and 1.0, an interpolated [SliderSpec] is returned. + /// + /// If [other] is null, this method returns the current [SliderSpec] instance. + /// + /// The interpolation is performed on each property of the [SliderSpec] using the appropriate + /// interpolation method: + /// + /// - [BoxSpec.lerp] for [thumb] and [track] and [activeTrack] and [division]. + + /// 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 + /// from the [other] [SliderSpec] is used. + /// + /// This method is typically used in animations to smoothly transition between + /// different [SliderSpec] configurations. + @override + SliderSpec lerp(SliderSpec? other, double t) { + if (other == null) return _$this; + + return SliderSpec( + thumb: _$this.thumb.lerp(other.thumb, t), + track: _$this.track.lerp(other.track, t), + activeTrack: _$this.activeTrack.lerp(other.activeTrack, t), + division: _$this.division.lerp(other.division, t), + modifiers: other.modifiers, + animated: t < 0.5 ? _$this.animated : other.animated, + ); + } + + /// The list of properties that constitute the state of this [SliderSpec]. + /// + /// This property is used by the [==] operator and the [hashCode] getter to + /// compare two [SliderSpec] instances for equality. + @override + List get props => [ + _$this.thumb, + _$this.track, + _$this.activeTrack, + _$this.division, + _$this.modifiers, + _$this.animated, + ]; + + SliderSpec get _$this => this as SliderSpec; + + void _debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + .add(DiagnosticsProperty('thumb', _$this.thumb, defaultValue: null)); + properties + .add(DiagnosticsProperty('track', _$this.track, defaultValue: null)); + properties.add(DiagnosticsProperty('activeTrack', _$this.activeTrack, + defaultValue: null)); + properties.add( + DiagnosticsProperty('division', _$this.division, defaultValue: null)); + properties.add( + DiagnosticsProperty('modifiers', _$this.modifiers, defaultValue: null)); + properties.add( + DiagnosticsProperty('animated', _$this.animated, defaultValue: null)); + } +} + +/// Represents the attributes of a [SliderSpec]. +/// +/// This class encapsulates properties defining the layout and +/// appearance of a [SliderSpec]. +/// +/// Use this class to configure the attributes of a [SliderSpec] and pass it to +/// the [SliderSpec] constructor. +class SliderSpecAttribute extends SpecAttribute + with Diagnosticable { + final BoxSpecAttribute? thumb; + final BoxSpecAttribute? track; + final BoxSpecAttribute? activeTrack; + final BoxSpecAttribute? division; + + const SliderSpecAttribute({ + this.thumb, + this.track, + this.activeTrack, + this.division, + super.modifiers, + super.animated, + }); + + /// Resolves to [SliderSpec] using the provided [MixData]. + /// + /// If a property is null in the [MixData], it falls back to the + /// default value defined in the `defaultValue` for that property. + /// + /// ```dart + /// final sliderSpec = SliderSpecAttribute(...).resolve(mix); + /// ``` + @override + SliderSpec resolve(MixData mix) { + return SliderSpec( + thumb: thumb?.resolve(mix), + track: track?.resolve(mix), + activeTrack: activeTrack?.resolve(mix), + division: division?.resolve(mix), + modifiers: modifiers?.resolve(mix), + animated: animated?.resolve(mix) ?? mix.animation, + ); + } + + /// Merges the properties of this [SliderSpecAttribute] with the properties of [other]. + /// + /// If [other] is null, returns this instance unchanged. Otherwise, returns a new + /// [SliderSpecAttribute] with the properties of [other] taking precedence over + /// the corresponding properties of this instance. + /// + /// Properties from [other] that are null will fall back + /// to the values from this instance. + @override + SliderSpecAttribute merge(covariant SliderSpecAttribute? other) { + if (other == null) return this; + + return SliderSpecAttribute( + thumb: thumb?.merge(other.thumb) ?? other.thumb, + track: track?.merge(other.track) ?? other.track, + activeTrack: activeTrack?.merge(other.activeTrack) ?? other.activeTrack, + division: division?.merge(other.division) ?? other.division, + modifiers: modifiers?.merge(other.modifiers) ?? other.modifiers, + animated: animated?.merge(other.animated) ?? other.animated, + ); + } + + /// The list of properties that constitute the state of this [SliderSpecAttribute]. + /// + /// This property is used by the [==] operator and the [hashCode] getter to + /// compare two [SliderSpecAttribute] instances for equality. + @override + List get props => [ + thumb, + track, + activeTrack, + division, + modifiers, + animated, + ]; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('thumb', thumb, defaultValue: null)); + properties.add(DiagnosticsProperty('track', track, defaultValue: null)); + properties.add( + DiagnosticsProperty('activeTrack', activeTrack, defaultValue: null)); + properties + .add(DiagnosticsProperty('division', division, defaultValue: null)); + properties + .add(DiagnosticsProperty('modifiers', modifiers, defaultValue: null)); + properties + .add(DiagnosticsProperty('animated', animated, defaultValue: null)); + } +} + +/// Utility class for configuring [SliderSpec] properties. +/// +/// This class provides methods to set individual properties of a [SliderSpec]. +/// Use the methods of this class to configure specific properties of a [SliderSpec]. +class SliderSpecUtility + extends SpecUtility { + /// Utility for defining [SliderSpecAttribute.thumb] + late final thumb = BoxSpecUtility((v) => only(thumb: v)); + + /// Utility for defining [SliderSpecAttribute.track] + late final track = BoxSpecUtility((v) => only(track: v)); + + /// Utility for defining [SliderSpecAttribute.activeTrack] + late final activeTrack = BoxSpecUtility((v) => only(activeTrack: v)); + + /// Utility for defining [SliderSpecAttribute.division] + late final division = BoxSpecUtility((v) => only(division: v)); + + /// Utility for defining [SliderSpecAttribute.modifiers] + late final wrap = SpecModifierUtility((v) => only(modifiers: v)); + + /// Utility for defining [SliderSpecAttribute.animated] + late final animated = AnimatedUtility((v) => only(animated: v)); + + SliderSpecUtility(super.builder, {super.mutable}); + + SliderSpecUtility get chain => + SliderSpecUtility(attributeBuilder, mutable: true); + + static SliderSpecUtility get self => + SliderSpecUtility((v) => v); + + /// Returns a new [SliderSpecAttribute] with the specified properties. + @override + T only({ + BoxSpecAttribute? thumb, + BoxSpecAttribute? track, + BoxSpecAttribute? activeTrack, + BoxSpecAttribute? division, + WidgetModifiersDataDto? modifiers, + AnimatedDataDto? animated, + }) { + return builder(SliderSpecAttribute( + thumb: thumb, + track: track, + activeTrack: activeTrack, + division: division, + modifiers: modifiers, + animated: animated, + )); + } +} + +/// A tween that interpolates between two [SliderSpec] instances. +/// +/// This class can be used in animations to smoothly transition between +/// different [SliderSpec] specifications. +class SliderSpecTween extends Tween { + SliderSpecTween({ + super.begin, + super.end, + }); + + @override + SliderSpec lerp(double t) { + if (begin == null && end == null) { + return SliderSpec(); + } + + if (begin == null) { + return end!; + } + + return begin!.lerp(end!, t); + } +} diff --git a/packages/remix/lib/src/components/slider/slider_style.dart b/packages/remix/lib/src/components/slider/slider_style.dart new file mode 100644 index 000000000..d17cecadf --- /dev/null +++ b/packages/remix/lib/src/components/slider/slider_style.dart @@ -0,0 +1,73 @@ +part of 'slider.dart'; + +class SliderStyle extends SpecStyle { + const SliderStyle(); + + @override + Style makeStyle(SpecConfiguration spec) { + final $ = spec.utilities; + + final divisions = $.division.chain + ..shape.circle() + ..color.black26() + ..height(3) + ..width(3); + + final thumb = $.thumb.chain + ..shape.circle() + ..color.white() + ..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.grey.shade300() + ..height(6) + ..borderRadius.all(10); + + final activeTrack = $.activeTrack.chain + ..color.black() + ..height(6) + ..borderRadius.all(10); + + final disabled = spec.on.disabled( + $.activeTrack.color.grey.shade500(), + $.thumb.shape.circle.side.color.grey.shade600(), + ); + + return Style.create([divisions, thumb, track, activeTrack, disabled]) + .animate( + duration: const Duration(milliseconds: 150), + curve: Curves.easeInOut, + ); + } +} + +class SliderDarkStyle extends SliderStyle { + const SliderDarkStyle(); + + @override + Style makeStyle(SpecConfiguration spec) { + final $ = spec.utilities; + final activeTrack = $.activeTrack.color.white(); + + final track = $.track.color.grey.shade800(); + + final thumb = $.thumb.chain + ..color.black() + ..shape.circle.side.color.white(); + + return Style.create([ + super.makeStyle(spec).call(), + activeTrack, + track, + thumb, + ]).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..8ac2a55a1 --- /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.$accent(7), + $.thumb.shape.circle.side.color.$accent(7), + ); + + final animation = (spec.on.press | spec.on.hover)( + $.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 new file mode 100644 index 000000000..89cfb2db5 --- /dev/null +++ b/packages/remix/lib/src/components/slider/slider_widget.dart @@ -0,0 +1,161 @@ +part of 'slider.dart'; + +class Slider extends StatefulWidget { + const Slider({ + super.key, + this.min = 0.0, + this.max = 1.0, + this.divisions = 0, + required this.onChanged, + required this.value, + this.onChangeEnd, + this.onChangeStart, + this.style, + this.variants = const [], + this.disabled = false, + }); + + final double min; + final double max; + final int divisions; + final double value; + final SliderStyle? style; + final List variants; + final bool disabled; + + final ValueChanged? onChanged; + final ValueChanged? onChangeEnd; + final ValueChanged? onChangeStart; + + @override + State createState() => _SliderState(); +} + +class _SliderState extends State with TickerProviderStateMixin { + double _sliderWidth = 0.0; + double _thumbWidth = 0.0; + + late MixWidgetStateController _controller; + @override + void initState() { + super.initState(); + _controller = MixWidgetStateController()..disabled = widget.disabled; + } + + double _calculateValue(Offset localPosition) { + double dx = (localPosition.dx - _thumbWidth / 2).clamp(0, _sliderWidth); + double percent = dx / _sliderWidth; + int divisions = widget.divisions; + + if (divisions > 0) { + final range = widget.max - widget.min; + final step = range / divisions; + final rounded = ((percent * range) / step).round(); + + return step * rounded; + } + + return widget.min + percent * (widget.max - widget.min); + } + + void _handleInteraction(void Function(MixWidgetStateController) callback) { + if (_controller.disabled) return; + callback(_controller); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final style = widget.style ?? context.remix.components.slider; + final configuration = SpecConfiguration(context, SliderSpecUtility.self); + + return Interactable( + enabled: !widget.disabled, + mouseCursor: widget.disabled + ? SystemMouseCursors.forbidden + : SystemMouseCursors.click, + controller: _controller, + child: GestureDetector( + onPanStart: (details) { + _handleInteraction((c) { + c.pressed = true; + final value = _calculateValue(details.localPosition); + widget.onChangeStart?.call(value); + }); + }, + onPanUpdate: (details) { + _handleInteraction((c) { + c.pressed = true; + final value = _calculateValue(details.localPosition); + widget.onChanged?.call(value); + }); + }, + onPanEnd: (details) { + _handleInteraction((c) { + c.pressed = false; + }); + }, + child: SpecBuilder( + style: style.makeStyle(configuration).applyVariants(widget.variants), + builder: (context) { + final spec = SliderSpec.of(context); + _thumbWidth = spec.thumb.width ?? 0; + + return Padding( + padding: EdgeInsets.symmetric( + horizontal: (spec.thumb.width ?? 0) / 2, + ), + child: LayoutBuilder( + builder: (context, constraints) { + _sliderWidth = constraints.maxWidth; + double percent = + ((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(a * (i + 1), 0), + child: spec.division(), + ), + ]; + + return Container( + color: Colors.transparent, + height: (spec.thumb.height ?? 0) * 2, + child: Stack( + alignment: Alignment.centerLeft, + children: [ + spec.track(), + ...divisions, + FractionallySizedBox( + alignment: Alignment.centerLeft, + widthFactor: percent, + child: spec.activeTrack(), + ), + Transform.translate( + offset: Offset( + percent * constraints.maxWidth - _thumbWidth / 2, + 0, + ), + child: spec.thumb(), + ), + ], + ), + ); + }, + ), + ); + }, + ), + ), + ); + } +} diff --git a/packages/remix/lib/src/theme/remix_theme.dart b/packages/remix/lib/src/theme/remix_theme.dart index dc0629e76..e477250c6 100644 --- a/packages/remix/lib/src/theme/remix_theme.dart +++ b/packages/remix/lib/src/theme/remix_theme.dart @@ -19,6 +19,7 @@ import '../components/radio/radio.dart'; import '../components/scaffold/scaffold.dart'; import '../components/segmented_control/segmented_control.dart'; import '../components/select/select.dart'; +import '../components/slider/slider.dart'; import '../components/spinner/spinner.dart'; import '../components/switch/switch.dart'; import '../components/toast/toast.dart'; @@ -45,6 +46,7 @@ class RemixComponentTheme { final SpinnerStyle spinner; final SwitchStyle switchComponent; final ToastStyle toast; + final SliderStyle slider; const RemixComponentTheme({ required this.accordion, @@ -67,6 +69,7 @@ class RemixComponentTheme { required this.spinner, required this.switchComponent, required this.toast, + required this.slider, }); factory RemixComponentTheme.baseLight() { @@ -91,6 +94,7 @@ class RemixComponentTheme { spinner: SpinnerStyle(), switchComponent: SwitchStyle(), toast: ToastStyle(), + slider: SliderStyle(), ); } @@ -115,6 +119,7 @@ class RemixComponentTheme { select: const SelectDarkStyle(), spinner: const SpinnerDarkStyle(), switchComponent: const SwitchDarkStyle(), + slider: const SliderDarkStyle(), ); } @@ -140,6 +145,7 @@ class RemixComponentTheme { spinner: FortalezaSpinnerStyle(), switchComponent: FortalezaSwitchStyle(), toast: FortalezaToastStyle(), + slider: FortalezaSliderStyle(), ); } @@ -174,6 +180,7 @@ class RemixComponentTheme { SpinnerStyle? spinner, SwitchStyle? switchComponent, ToastStyle? toast, + SliderStyle? slider, }) { return RemixComponentTheme( accordion: accordion ?? this.accordion, @@ -196,6 +203,7 @@ class RemixComponentTheme { spinner: spinner ?? this.spinner, switchComponent: switchComponent ?? this.switchComponent, toast: toast ?? this.toast, + slider: slider ?? this.slider, ); } }