diff --git a/example/lib/home.dart b/example/lib/home.dart index de428915..19c12839 100644 --- a/example/lib/home.dart +++ b/example/lib/home.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:zeta_example/pages/components/accordion_example.dart'; +import 'package:zeta_example/pages/components/app_bar_example.dart'; import 'package:zeta_example/pages/components/avatar_example.dart'; import 'package:zeta_example/pages/components/badges_example.dart'; import 'package:zeta_example/pages/components/banner_example.dart'; @@ -18,6 +19,7 @@ import 'package:zeta_example/pages/components/navigation_bar_example.dart'; import 'package:zeta_example/pages/components/navigation_rail_example.dart'; import 'package:zeta_example/pages/components/phone_input_example.dart'; import 'package:zeta_example/pages/components/radio_example.dart'; +import 'package:zeta_example/pages/components/segmented_control_example.dart'; import 'package:zeta_example/pages/components/stepper_example.dart'; import 'package:zeta_example/pages/components/switch_example.dart'; import 'package:zeta_example/pages/components/snackbar_example.dart'; @@ -43,6 +45,7 @@ class Component { final List components = [ Component(AccordionExample.name, (context) => const AccordionExample()), + Component(AppBarExample.name, (context) => const AppBarExample()), Component(AvatarExample.name, (context) => const AvatarExample()), Component(BannerExample.name, (context) => const BannerExample()), Component(BadgesExample.name, (context) => const BadgesExample()), @@ -57,6 +60,7 @@ final List components = [ Component(PasswordInputExample.name, (context) => const PasswordInputExample()), Component(DropdownExample.name, (context) => const DropdownExample()), Component(ProgressExample.name, (context) => const ProgressExample()), + Component(SegmentedControlExample.name, (context) => const SegmentedControlExample()), Component(SnackBarExample.name, (context) => const SnackBarExample()), Component(StepperExample.name, (context) => const StepperExample()), Component(TabsExample.name, (context) => const TabsExample()), diff --git a/example/lib/pages/components/segmented_control_example.dart b/example/lib/pages/components/segmented_control_example.dart new file mode 100644 index 00000000..783e966c --- /dev/null +++ b/example/lib/pages/components/segmented_control_example.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:zeta_example/widgets.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +class SegmentedControlExample extends StatefulWidget { + const SegmentedControlExample({super.key}); + + static const String name = 'SegmentedControl'; + + @override + State createState() => _SegmentedControlExampleState(); +} + +class _SegmentedControlExampleState extends State { + final _iconsSegments = [1, 2, 3, 4, 5]; + final _numberSegments = [1, 2, 3, 4, 5]; + late int _selectedIconSegment = _iconsSegments.first; + late int _selectedNumberSegment = _numberSegments.first; + late String _selectedTextSegment = _textSegments.first; + final _textSegments = ["Item 1", "Item 2", "Item 3"]; + + @override + Widget build(BuildContext context) { + return ExampleScaffold( + name: SegmentedControlExample.name, + child: SingleChildScrollView( + child: Column( + children: [ + // Text + Padding( + padding: const EdgeInsets.all(ZetaSpacing.l), + child: ZetaSegmentedControl( + segments: [ + for (final value in _textSegments) + ZetaButtonSegment( + value: value, + child: Text(value), + ), + ], + onChanged: (value) => setState( + () => _selectedTextSegment = value, + ), + selected: _selectedTextSegment, + ), + ), + + // Numbers + Padding( + padding: const EdgeInsets.all(ZetaSpacing.l), + child: ZetaSegmentedControl( + segments: [ + for (final value in _numberSegments) + ZetaButtonSegment( + value: value, + child: Text(value.toString()), + ), + ], + onChanged: (value) => setState( + () => _selectedNumberSegment = value, + ), + selected: _selectedNumberSegment, + ), + ), + + // Icons + Padding( + padding: const EdgeInsets.all(ZetaSpacing.l), + child: ZetaSegmentedControl( + segments: [ + for (final value in _iconsSegments) + ZetaButtonSegment( + value: value, + child: Icon(ZetaIcons.star_round), + ), + ], + onChanged: (value) => setState( + () => _selectedIconSegment = value, + ), + selected: _selectedIconSegment, + ), + ), + ], + ), + ), + ); + } +} diff --git a/example/widgetbook/main.dart b/example/widgetbook/main.dart index f5975bb9..172069e5 100644 --- a/example/widgetbook/main.dart +++ b/example/widgetbook/main.dart @@ -26,6 +26,7 @@ import 'pages/components/password_input_widgetbook.dart'; import 'pages/components/phone_input_widgetbook.dart'; import 'pages/components/progress_widgetbook.dart'; import 'pages/components/radio_widgetbook.dart'; +import 'pages/components/segmented_control_widgetbook.dart'; import 'pages/components/stepper_widgetbook.dart'; import 'pages/components/switch_widgetbook.dart'; import 'pages/components/snack_bar_widgetbook.dart'; @@ -115,6 +116,10 @@ class HotReload extends StatelessWidget { ], ), WidgetbookUseCase(name: 'Radio Button', builder: (context) => radioButtonUseCase(context)), + WidgetbookUseCase( + name: 'Segmented Control', + builder: (context) => segmentedControlUseCase(context), + ), WidgetbookUseCase(name: 'Switch', builder: (context) => switchUseCase(context)), WidgetbookUseCase( name: 'Snack Bar', diff --git a/example/widgetbook/pages/components/segmented_control_widgetbook.dart b/example/widgetbook/pages/components/segmented_control_widgetbook.dart new file mode 100644 index 00000000..e3544fa7 --- /dev/null +++ b/example/widgetbook/pages/components/segmented_control_widgetbook.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:widgetbook/widgetbook.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +import '../../test/test_components.dart'; +import '../../utils/utils.dart'; + +Widget segmentedControlUseCase(BuildContext context) { + final iconsSegments = List.generate(5, (index) => index); + int selectedIconSegment = iconsSegments.first; + + final rounded = context.knobs.boolean(label: "Rounded", initialValue: true); + final icon = iconKnob(context, rounded: rounded, initial: ZetaIcons.star_round); + + final text = context.knobs.string(label: 'Text', initialValue: "Item"); + + final textSegments = List.generate(3, (index) => "$text ${index + 1}"); + String selectedTextSegment = textSegments.first; + + return WidgetbookTestWidget( + widget: StatefulBuilder(builder: (context, setState) { + return Column( + children: [ + Padding( + padding: const EdgeInsets.all(ZetaSpacing.l), + child: ZetaSegmentedControl( + rounded: rounded, + segments: [ + for (final value in iconsSegments) + ZetaButtonSegment( + value: value, + child: Icon(icon), + ), + ], + onChanged: (value) => setState( + () => selectedIconSegment = value, + ), + selected: selectedIconSegment, + ), + ), + Padding( + padding: const EdgeInsets.all(ZetaSpacing.l), + child: ZetaSegmentedControl( + rounded: rounded, + segments: [ + for (final value in textSegments) + ZetaButtonSegment( + value: value, + child: Text( + value, + ), + ), + ], + onChanged: (value) => setState( + () => selectedTextSegment = value, + ), + selected: selectedTextSegment, + ), + ), + ], + ); + }), + ); +} diff --git a/lib/src/components/segmented_control/segmented_control.dart b/lib/src/components/segmented_control/segmented_control.dart new file mode 100644 index 00000000..43868eec --- /dev/null +++ b/lib/src/components/segmented_control/segmented_control.dart @@ -0,0 +1,621 @@ +import 'dart:math' as math; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/physics.dart'; +import 'package:flutter/rendering.dart'; +import '../../../zeta_flutter.dart'; + +/// Creates an segmented control bar. +class ZetaSegmentedControl extends StatefulWidget { + /// Constructs an segmented control bar. + const ZetaSegmentedControl({ + required this.segments, + required this.onChanged, + required this.selected, + this.rounded = true, + super.key, + }); + + /// The callback that is called when a new option is tapped. + final void Function(T)? onChanged; + + /// Whether the corners to be rounded. + final bool rounded; + + /// Descriptions of the segments in the button. + final List> segments; + + /// Currently selected segment. + final T selected; + + @override + State> createState() => _ZetaSegmentedControlState(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add( + ObjectFlagProperty.has('onChanged', onChanged), + ) + ..add(DiagnosticsProperty('rounded', rounded)) + ..add(IterableProperty>('segments', segments)) + ..add(DiagnosticsProperty('selected', selected)); + } +} + +class _ZetaSegmentedControlState extends State> + with TickerProviderStateMixin> { + T? _highlighted; + Animatable? _thumbAnimatable; + late final AnimationController _thumbController = AnimationController( + duration: kThemeAnimationDuration, + value: 0, + vsync: this, + ); + + late final _thumbScaleAnimation = _thumbScaleController.drive(Tween(begin: 1)); + + late final _thumbScaleController = AnimationController( + duration: kThemeAnimationDuration, + value: 0, + vsync: this, + ); + + @override + void initState() { + super.initState(); + + _highlighted = widget.selected; + } + + @override + void didUpdateWidget(ZetaSegmentedControl oldWidget) { + super.didUpdateWidget(oldWidget); + + if (_highlighted != widget.selected) { + _thumbController.animateWith( + SpringSimulation( + const SpringDescription(mass: 1, stiffness: 500, damping: 44), + 0, + 1, + 0, // Every time a new spring animation starts the previous animation stops. + ), + ); + _thumbAnimatable = null; + _highlighted = widget.selected; + } + } + + @override + void dispose() { + _thumbScaleController.dispose(); + _thumbController.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final List children = []; + int index = 0; + int? highlightedIndex; + for (final segment in widget.segments) { + final isHighlighted = _highlighted == segment.value; + if (isHighlighted) highlightedIndex = index; + + if (index != 0) { + children.add(SizedBox(key: Key(index.toString()))); + } + + children.add( + _Segment( + key: ValueKey(segment.value), + rounded: widget.rounded, + child: segment.child, + onTap: () => widget.onChanged?.call(segment.value), + ), + ); + + index += 1; + } + + final colors = Zeta.of(context).colors; + + return MouseRegion( + cursor: kIsWeb ? SystemMouseCursors.click : MouseCursor.defer, + child: SelectionContainer.disabled( + child: Container( + padding: const EdgeInsets.all(ZetaSpacing.xxs), + decoration: BoxDecoration( + color: colors.surfaceDisabled, + borderRadius: widget.rounded ? ZetaRadius.minimal : ZetaRadius.none, + ), + child: AnimatedBuilder( + animation: _thumbScaleAnimation, + builder: (BuildContext context, Widget? child) { + return _SegmentedControlRenderWidget( + highlightedIndex: highlightedIndex, + thumbColor: colors.surfacePrimary, + thumbScale: _thumbScaleAnimation.value, + rounded: widget.rounded, + state: this, + children: children, + ); + }, + ), + ), + ), + ); + } +} + +/// Data describing a segment of a [ZetaSegmentedControl]. +class ZetaButtonSegment { + /// Construct a [ZetaButtonSegment]. + const ZetaButtonSegment({ + required this.value, + required this.child, + }); + + /// The child to be displayed + final Widget child; + + /// Value used to identify the segment. + /// + /// This value must be unique across all segments. + final T value; +} + +class _Segment extends StatefulWidget { + const _Segment({ + required ValueKey key, + required this.child, + required this.rounded, + required this.onTap, + }) : super(key: key); + + final Widget child; + final bool rounded; + final VoidCallback onTap; + + @override + _SegmentState createState() => _SegmentState(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('rounded', rounded)) + ..add(ObjectFlagProperty.has('onTap', onTap)); + } +} + +class _SegmentState extends State<_Segment> with TickerProviderStateMixin<_Segment> { + @override + Widget build(BuildContext context) { + final colors = Zeta.of(context).colors; + + return Material( + color: Colors.transparent, + child: InkWell( + splashFactory: NoSplash.splashFactory, + borderRadius: widget.rounded ? ZetaRadius.minimal : ZetaRadius.none, + onTap: widget.onTap, + child: IndexedStack( + alignment: Alignment.center, + children: [ + widget.child, + IconTheme( + data: const IconThemeData(size: ZetaSpacing.x5), + child: DefaultTextStyle( + style: ZetaTextStyles.labelMedium.copyWith( + color: colors.textDefault, + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: ZetaSpacing.l, + vertical: ZetaSpacing.xxs, + ), + child: widget.child, + ), + ), + ), + ], + ), + ), + ); + } +} + +class _SegmentedControlRenderWidget extends MultiChildRenderObjectWidget { + const _SegmentedControlRenderWidget({ + super.key, + super.children, + required this.highlightedIndex, + required this.thumbColor, + required this.thumbScale, + required this.state, + required this.rounded, + }); + + final int? highlightedIndex; + final bool rounded; + final _ZetaSegmentedControlState state; + final Color thumbColor; + final double thumbScale; + + @override + RenderObject createRenderObject(BuildContext context) { + return _RenderSegmentedControl( + highlightedIndex: highlightedIndex, + thumbColor: thumbColor, + rounded: rounded, + state: state, + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(IntProperty('highlightedIndex', highlightedIndex)) + ..add(DiagnosticsProperty('rounded', rounded)) + ..add(DiagnosticsProperty<_ZetaSegmentedControlState>('state', state)) + ..add(ColorProperty('thumbColor', thumbColor)) + ..add(DoubleProperty('thumbScale', thumbScale)); + } + + @override + void updateRenderObject( + BuildContext context, + _RenderSegmentedControl renderObject, + ) { + renderObject + ..thumbColor = thumbColor + ..highlightedIndex = highlightedIndex; + } +} + +class _SegmentedControlContainerBoxParentData extends ContainerBoxParentData {} + +class _RenderSegmentedControl extends RenderBox + with + ContainerRenderObjectMixin>, + RenderBoxContainerDefaultsMixin> { + _RenderSegmentedControl({ + required int? highlightedIndex, + required Color thumbColor, + required this.rounded, + required this.state, + }) : _highlightedIndex = highlightedIndex, + _thumbColor = thumbColor; + + // The current **Unscaled** Thumb Rect in this RenderBox's coordinate space. + Rect? currentThumbRect; + + /// Wether the corners to be rounded. + final bool rounded; + + // Paint the separator to the right of the given child. + final Paint separatorPaint = Paint(); + + final _ZetaSegmentedControlState state; + + int? _highlightedIndex; + Color _thumbColor; + + @override + void attach(PipelineOwner owner) { + super.attach(owner); + state._thumbController.addListener(markNeedsPaint); + } + + @override + double? computeDistanceToActualBaseline(TextBaseline baseline) { + return defaultComputeDistanceToHighestActualBaseline(baseline); + } + + @override + Size computeDryLayout(BoxConstraints constraints) { + final Size childSize = _calculateChildSize(constraints); + return _computeOverallSizeFromChildSize(childSize, constraints); + } + + @override + double computeMaxIntrinsicHeight(double width) { + RenderBox? child = firstChild; + double maxMaxChildHeight = ZetaSpacing.x7; + while (child != null) { + final double childHeight = child.getMaxIntrinsicHeight(width); + maxMaxChildHeight = math.max(maxMaxChildHeight, childHeight); + child = nonSeparatorChildAfter(child); + } + return maxMaxChildHeight; + } + + @override + double computeMaxIntrinsicWidth(double height) { + final int childCount = this.childCount ~/ 2 + 1; + RenderBox? child = firstChild; + double maxMaxChildWidth = 0; + while (child != null) { + final double childWidth = child.getMaxIntrinsicWidth(height); + maxMaxChildWidth = math.max(maxMaxChildWidth, childWidth); + child = nonSeparatorChildAfter(child); + } + return maxMaxChildWidth * childCount + totalSeparatorWidth; + } + + @override + double computeMinIntrinsicHeight(double width) { + RenderBox? child = firstChild; + double maxMinChildHeight = ZetaSpacing.x7; + while (child != null) { + final double childHeight = child.getMinIntrinsicHeight(width); + maxMinChildHeight = math.max(maxMinChildHeight, childHeight); + child = nonSeparatorChildAfter(child); + } + return maxMinChildHeight; + } + + @override + double computeMinIntrinsicWidth(double height) { + final int childCount = this.childCount ~/ 2 + 1; + RenderBox? child = firstChild; + double maxMinChildWidth = 0; + while (child != null) { + final double childWidth = child.getMinIntrinsicWidth(height); + maxMinChildWidth = math.max(maxMinChildWidth, childWidth); + child = nonSeparatorChildAfter(child); + } + return (maxMinChildWidth + 2 * ZetaSpacing.l) * childCount + totalSeparatorWidth; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('currentThumbRect', currentThumbRect)) + ..add(DiagnosticsProperty('rounded', rounded)) + ..add(DiagnosticsProperty('separatorPaint', separatorPaint)) + ..add(DiagnosticsProperty<_ZetaSegmentedControlState>('state', state)) + ..add(IntProperty('highlightedIndex', highlightedIndex)) + ..add(ColorProperty('thumbColor', thumbColor)) + ..add(DoubleProperty('totalSeparatorWidth', totalSeparatorWidth)); + } + + @override + void detach() { + state._thumbController.removeListener(markNeedsPaint); + super.detach(); + } + + @override + bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { + RenderBox? child = lastChild; + while (child != null) { + final _SegmentedControlContainerBoxParentData childParentData = + child.parentData! as _SegmentedControlContainerBoxParentData; + if ((childParentData.offset & child.size).contains(position)) { + return result.addWithPaintOffset( + offset: childParentData.offset, + position: position, + hitTest: (BoxHitTestResult result, Offset localOffset) { + return child!.hitTest(result, position: localOffset); + }, + ); + } + child = childParentData.previousSibling; + } + return false; + } + + @override + void paint(PaintingContext context, Offset offset) { + final List children = getChildrenAsList(); + + for (int index = 1; index < childCount; index += 2) { + _paintSeparator(context, offset, children[index]); + } + + final int? highlightedChildIndex = highlightedIndex; + // Paint thumb if there's a highlighted segment. + if (highlightedChildIndex != null) { + final RenderBox selectedChild = children[highlightedChildIndex * 2]; + + final _SegmentedControlContainerBoxParentData childParentData = + selectedChild.parentData! as _SegmentedControlContainerBoxParentData; + final newThumbRect = childParentData.offset & selectedChild.size; + + // Update thumb animation's tween, in case the end rect changed (e.g., a + // new segment is added during the animation). + if (state._thumbController.isAnimating) { + final Animatable? thumbTween = state._thumbAnimatable; + if (thumbTween == null) { + // This is the first frame of the animation. + final Rect startingRect = moveThumbRectInBound(currentThumbRect, children) ?? newThumbRect; + state._thumbAnimatable = RectTween(begin: startingRect, end: newThumbRect); + } else if (newThumbRect != thumbTween.transform(1)) { + // The thumbTween of the running sliding animation needs updating, + // without restarting the animation. + final Rect startingRect = moveThumbRectInBound(currentThumbRect, children) ?? newThumbRect; + state._thumbAnimatable = RectTween(begin: startingRect, end: newThumbRect).chain( + CurveTween(curve: Interval(state._thumbController.value, 1)), + ); + } + } else { + state._thumbAnimatable = null; + } + + final Rect unscaledThumbRect = state._thumbAnimatable?.evaluate(state._thumbController) ?? newThumbRect; + currentThumbRect = unscaledThumbRect; + final Rect thumbRect = Rect.fromCenter( + center: unscaledThumbRect.center, + width: unscaledThumbRect.width, + height: unscaledThumbRect.height, + ); + + _paintThumb(context, offset, thumbRect); + } else { + currentThumbRect = null; + } + + for (int index = 0; index < children.length; index += 2) { + _paintChild(context, offset, children[index]); + } + } + + @override + void performLayout() { + final BoxConstraints constraints = this.constraints; + final Size childSize = _calculateChildSize(constraints); + final BoxConstraints childConstraints = BoxConstraints.tight(childSize); + final BoxConstraints separatorConstraints = childConstraints.heightConstraints(); + + RenderBox? child = firstChild; + int index = 0; + double start = 0; + while (child != null) { + child.layout( + index.isEven ? childConstraints : separatorConstraints, + parentUsesSize: true, + ); + final _SegmentedControlContainerBoxParentData childParentData = + child.parentData! as _SegmentedControlContainerBoxParentData; + final Offset childOffset = Offset(start, 0); + childParentData.offset = childOffset; + start += child.size.width; + + child = childAfter(child); + index += 1; + } + + size = _computeOverallSizeFromChildSize(childSize, constraints); + } + + @override + void setupParentData(RenderBox child) { + if (child.parentData is! _SegmentedControlContainerBoxParentData) { + child.parentData = _SegmentedControlContainerBoxParentData(); + } + } + + int? get highlightedIndex => _highlightedIndex; + + set highlightedIndex(int? value) { + if (_highlightedIndex == value) { + return; + } + + _highlightedIndex = value; + markNeedsPaint(); + } + + Color get thumbColor => _thumbColor; + + set thumbColor(Color value) { + if (_thumbColor == value) { + return; + } + _thumbColor = value; + markNeedsPaint(); + } + + double get totalSeparatorWidth => 0.0 * (childCount ~/ 2); + + RenderBox? nonSeparatorChildAfter(RenderBox child) { + final RenderBox? nextChild = childAfter(child); + return nextChild == null ? null : childAfter(nextChild); + } + + // This method is used to convert the original unscaled thumb rect painted in + // the previous frame, to a Rect that is within the valid boundary defined by + // the child segments. + // + // The overall size does not include that of the thumb. That is, if the thumb + // is located at the first or the last segment, the thumb can get cut off if + // one of the values in _kThumbInsets is positive. + Rect? moveThumbRectInBound(Rect? thumbRect, List children) { + if (thumbRect == null) { + return null; + } + + final Offset firstChildOffset = (children.first.parentData! as _SegmentedControlContainerBoxParentData).offset; + final double leftMost = firstChildOffset.dx; + final double rightMost = + (children.last.parentData! as _SegmentedControlContainerBoxParentData).offset.dx + children.last.size.width; + + // Ignore the horizontal position and the height of `thumbRect`, and + // calculates them from `children`. + return Rect.fromLTRB( + math.max(thumbRect.left, leftMost), + firstChildOffset.dy, + math.min(thumbRect.right, rightMost), + firstChildOffset.dy + children.first.size.height, + ); + } + + Size _calculateChildSize(BoxConstraints constraints) { + final int childCount = this.childCount ~/ 2 + 1; + double childWidth = (constraints.minWidth - totalSeparatorWidth) / childCount; + double maxHeight = ZetaSpacing.x7; + RenderBox? child = firstChild; + while (child != null) { + childWidth = math.max(childWidth, child.getMaxIntrinsicWidth(double.infinity) + 2); + child = nonSeparatorChildAfter(child); + } + childWidth = math.min( + childWidth, + (constraints.maxWidth - totalSeparatorWidth) / childCount, + ); + child = firstChild; + while (child != null) { + final double boxHeight = child.getMaxIntrinsicHeight(childWidth); + maxHeight = math.max(maxHeight, boxHeight); + child = nonSeparatorChildAfter(child); + } + return Size(childWidth, maxHeight); + } + + Size _computeOverallSizeFromChildSize( + Size childSize, + BoxConstraints constraints, + ) { + final int childCount = this.childCount ~/ 2 + 1; + return constraints.constrain( + Size( + childSize.width * childCount + totalSeparatorWidth, + childSize.height, + ), + ); + } + + void _paintSeparator( + PaintingContext context, + Offset offset, + RenderBox child, + ) { + final _SegmentedControlContainerBoxParentData childParentData = + child.parentData! as _SegmentedControlContainerBoxParentData; + context.paintChild(child, offset + childParentData.offset); + } + + void _paintChild(PaintingContext context, Offset offset, RenderBox child) { + final _SegmentedControlContainerBoxParentData childParentData = + child.parentData! as _SegmentedControlContainerBoxParentData; + context.paintChild(child, childParentData.offset + offset); + } + + void _paintThumb(PaintingContext context, Offset offset, Rect thumbRect) { + final RRect thumbRRect = RRect.fromRectAndRadius( + thumbRect.shift(offset), + rounded ? ZetaRadius.minimal.topLeft : ZetaRadius.none.topLeft, + ); + + context.canvas.drawRRect( + thumbRRect, + Paint()..color = thumbColor, + ); + } +} diff --git a/lib/zeta_flutter.dart b/lib/zeta_flutter.dart index 3638bf90..b722a8b3 100644 --- a/lib/zeta_flutter.dart +++ b/lib/zeta_flutter.dart @@ -36,6 +36,7 @@ export 'src/components/phone_input/phone_input.dart'; export 'src/components/progress/progress_bar.dart'; export 'src/components/progress/progress_circle.dart'; export 'src/components/radio/radio.dart'; +export 'src/components/segmented_control/segmented_control.dart'; export 'src/components/snack_bar/snack_bar.dart'; export 'src/components/stepper/stepper.dart'; export 'src/components/switch/zeta_switch.dart';