1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/// @docImport 'switch.dart';
6library;
7
8import 'dart:collection';
9import 'dart:math' as math;
10
11import 'package:flutter/foundation.dart';
12import 'package:flutter/rendering.dart';
13import 'package:flutter/widgets.dart';
14
15import 'theme.dart';
16
17// Minimum padding from edges of the segmented control to edges of
18// encompassing widget.
19const EdgeInsetsGeometry _kHorizontalItemPadding = EdgeInsets.symmetric(horizontal: 16.0);
20
21// Minimum height of the segmented control.
22const double _kMinSegmentedControlHeight = 28.0;
23
24// The default color used for the text of the disabled segment.
25const Color _kDisableTextColor = Color.fromARGB(115, 122, 122, 122);
26
27// The duration of the fade animation used to transition when a new widget
28// is selected.
29const Duration _kFadeDuration = Duration(milliseconds: 165);
30
31/// An iOS-style segmented control.
32///
33/// Displays the widgets provided in the [Map] of [children] in a
34/// horizontal list. Used to select between a number of mutually exclusive
35/// options. When one option in the segmented control is selected, the other
36/// options in the segmented control cease to be selected.
37///
38/// A segmented control can feature any [Widget] as one of the values in its
39/// [Map] of [children]. The type T is the type of the keys used
40/// to identify each widget and determine which widget is selected. As
41/// required by the [Map] class, keys must be of consistent types
42/// and must be comparable. The ordering of the keys will determine the order
43/// of the widgets in the segmented control.
44///
45/// When the state of the segmented control changes, the widget calls the
46/// [onValueChanged] callback. The map key associated with the newly selected
47/// widget is returned in the [onValueChanged] callback. Typically, widgets
48/// that use a segmented control will listen for the [onValueChanged] callback
49/// and rebuild the segmented control with a new [groupValue] to update which
50/// option is currently selected.
51///
52/// The [children] will be displayed in the order of the keys in the [Map].
53/// The height of the segmented control is determined by the height of the
54/// tallest widget provided as a value in the [Map] of [children].
55/// The width of each child in the segmented control will be equal to the width
56/// of widest child, unless the combined width of the children is wider than
57/// the available horizontal space. In this case, the available horizontal space
58/// is divided by the number of provided [children] to determine the width of
59/// each widget. The selection area for each of the widgets in the [Map] of
60/// [children] will then be expanded to fill the calculated space, so each
61/// widget will appear to have the same dimensions.
62///
63/// A segmented control may optionally be created with custom colors. The
64/// [unselectedColor], [selectedColor], [borderColor], and [pressedColor]
65/// arguments can be used to override the segmented control's colors from
66/// [CupertinoTheme] defaults. The [disabledColor] and [disabledTextColor]
67/// set the background and text colors of the segment when it is disabled.
68///
69/// The segmented control can be disabled by adding children to the [Set] of
70/// [disabledChildren]. If the child is not present in the [Set], it is enabled
71/// by default.
72///
73/// {@tool dartpad}
74/// This example shows a [CupertinoSegmentedControl] with an enum type.
75///
76/// The callback provided to [onValueChanged] should update the state of
77/// the parent [StatefulWidget] using the [State.setState] method, so that
78/// the parent gets rebuilt.
79///
80/// This example also demonstrates how to use the [disabledChildren] property by
81/// toggling each [CupertinoSwitch] to enable or disable the segments.
82///
83/// ** See code in examples/api/lib/cupertino/segmented_control/cupertino_segmented_control.0.dart **
84/// {@end-tool}
85///
86/// See also:
87///
88/// * [CupertinoSegmentedControl], a segmented control widget in the style used
89/// up until iOS 13.
90/// * <https://developer.apple.com/design/human-interface-guidelines/ios/controls/segmented-controls/>
91class CupertinoSegmentedControl<T extends Object> extends StatefulWidget {
92 /// Creates an iOS-style segmented control bar.
93 ///
94 /// The [children] argument must be an ordered [Map] such as a
95 /// [LinkedHashMap]. Further, the length of the [children] list must be
96 /// greater than one.
97 ///
98 /// Each widget value in the map of [children] must have an associated key
99 /// that uniquely identifies this widget. This key is what will be returned
100 /// in the [onValueChanged] callback when a new value from the [children] map
101 /// is selected.
102 ///
103 /// The [groupValue] is the currently selected value for the segmented control.
104 /// If no [groupValue] is provided, or the [groupValue] is null, no widget will
105 /// appear as selected. The [groupValue] must be either null or one of the keys
106 /// in the [children] map.
107 CupertinoSegmentedControl({
108 super.key,
109 required this.children,
110 required this.onValueChanged,
111 this.groupValue,
112 this.unselectedColor,
113 this.selectedColor,
114 this.borderColor,
115 this.pressedColor,
116 this.disabledColor,
117 this.disabledTextColor,
118 this.padding,
119 this.disabledChildren = const <Never>{},
120 }) : assert(children.length >= 2),
121 assert(
122 groupValue == null || children.keys.any((T child) => child == groupValue),
123 'The groupValue must be either null or one of the keys in the children map.',
124 );
125
126 /// The identifying keys and corresponding widget values in the
127 /// segmented control.
128 ///
129 /// The map must have more than one entry.
130 /// This attribute must be an ordered [Map] such as a [LinkedHashMap].
131 final Map<T, Widget> children;
132
133 /// The identifier of the widget that is currently selected.
134 ///
135 /// This must be one of the keys in the [Map] of [children].
136 /// If this attribute is null, no widget will be initially selected.
137 final T? groupValue;
138
139 /// The callback that is called when a new option is tapped.
140 ///
141 /// The segmented control passes the newly selected widget's associated key
142 /// to the callback but does not actually change state until the parent
143 /// widget rebuilds the segmented control with the new [groupValue].
144 final ValueChanged<T> onValueChanged;
145
146 /// The color used to fill the backgrounds of unselected widgets and as the
147 /// text color of the selected widget.
148 ///
149 /// Defaults to [CupertinoTheme]'s `primaryContrastingColor` if null.
150 final Color? unselectedColor;
151
152 /// The color used to fill the background of the selected widget and as the text
153 /// color of unselected widgets.
154 ///
155 /// Defaults to [CupertinoTheme]'s `primaryColor` if null.
156 final Color? selectedColor;
157
158 /// The color used as the border around each widget.
159 ///
160 /// Defaults to [CupertinoTheme]'s `primaryColor` if null.
161 final Color? borderColor;
162
163 /// The color used to fill the background of the widget the user is
164 /// temporarily interacting with through a long press or drag.
165 ///
166 /// Defaults to the selectedColor at 20% opacity if null.
167 final Color? pressedColor;
168
169 /// The color used to fill the background of the segment when it is disabled.
170 ///
171 /// If null, this color will be 50% opacity of the [selectedColor] when
172 /// the segment is selected. If the segment is unselected, this color will be
173 /// set to [unselectedColor].
174 final Color? disabledColor;
175
176 /// The color used for the text of the segment when it is disabled.
177 final Color? disabledTextColor;
178
179 /// The CupertinoSegmentedControl will be placed inside this padding.
180 ///
181 /// Defaults to EdgeInsets.symmetric(horizontal: 16.0)
182 final EdgeInsetsGeometry? padding;
183
184 /// The set of identifying keys that correspond to the segments that should be disabled.
185 ///
186 /// All segments are enabled by default.
187 final Set<T> disabledChildren;
188
189 @override
190 State<CupertinoSegmentedControl<T>> createState() => _SegmentedControlState<T>();
191}
192
193class _SegmentedControlState<T extends Object> extends State<CupertinoSegmentedControl<T>>
194 with TickerProviderStateMixin<CupertinoSegmentedControl<T>> {
195 T? _pressedKey;
196
197 final List<AnimationController> _selectionControllers = <AnimationController>[];
198 final List<ColorTween> _childTweens = <ColorTween>[];
199
200 late ColorTween _forwardBackgroundColorTween;
201 late ColorTween _reverseBackgroundColorTween;
202 late ColorTween _textColorTween;
203
204 Color? _selectedColor;
205 Color? _unselectedColor;
206 Color? _borderColor;
207 Color? _pressedColor;
208 Color? _selectedDisabledColor;
209 Color? _unselectedDisabledColor;
210 Color? _disabledTextColor;
211
212 AnimationController createAnimationController() {
213 return AnimationController(duration: _kFadeDuration, vsync: this)..addListener(() {
214 setState(() {
215 // State of background/text colors has changed
216 });
217 });
218 }
219
220 bool _updateColors() {
221 assert(mounted, 'This should only be called after didUpdateDependencies');
222 bool changed = false;
223 final Color disabledTextColor = widget.disabledTextColor ?? _kDisableTextColor;
224 if (_disabledTextColor != disabledTextColor) {
225 changed = true;
226 _disabledTextColor = disabledTextColor;
227 }
228 final Color selectedColor = widget.selectedColor ?? CupertinoTheme.of(context).primaryColor;
229 if (_selectedColor != selectedColor) {
230 changed = true;
231 _selectedColor = selectedColor;
232 }
233 final Color unselectedColor =
234 widget.unselectedColor ?? CupertinoTheme.of(context).primaryContrastingColor;
235 if (_unselectedColor != unselectedColor) {
236 changed = true;
237 _unselectedColor = unselectedColor;
238 }
239 final Color selectedDisabledColor = widget.disabledColor ?? selectedColor.withOpacity(0.5);
240 final Color unselectedDisabledColor = widget.disabledColor ?? unselectedColor;
241 if (_selectedDisabledColor != selectedDisabledColor ||
242 _unselectedDisabledColor != unselectedDisabledColor) {
243 changed = true;
244 _selectedDisabledColor = selectedDisabledColor;
245 _unselectedDisabledColor = unselectedDisabledColor;
246 }
247 final Color borderColor = widget.borderColor ?? CupertinoTheme.of(context).primaryColor;
248 if (_borderColor != borderColor) {
249 changed = true;
250 _borderColor = borderColor;
251 }
252 final Color pressedColor =
253 widget.pressedColor ?? CupertinoTheme.of(context).primaryColor.withOpacity(0.2);
254 if (_pressedColor != pressedColor) {
255 changed = true;
256 _pressedColor = pressedColor;
257 }
258
259 _forwardBackgroundColorTween = ColorTween(begin: _pressedColor, end: _selectedColor);
260 _reverseBackgroundColorTween = ColorTween(begin: _unselectedColor, end: _selectedColor);
261 _textColorTween = ColorTween(begin: _selectedColor, end: _unselectedColor);
262 return changed;
263 }
264
265 void _updateAnimationControllers() {
266 assert(mounted, 'This should only be called after didUpdateDependencies');
267 for (final AnimationController controller in _selectionControllers) {
268 controller.dispose();
269 }
270 _selectionControllers.clear();
271 _childTweens.clear();
272
273 for (final T key in widget.children.keys) {
274 final AnimationController animationController = createAnimationController();
275 if (widget.groupValue == key) {
276 _childTweens.add(_reverseBackgroundColorTween);
277 animationController.value = 1.0;
278 } else {
279 _childTweens.add(_forwardBackgroundColorTween);
280 }
281 _selectionControllers.add(animationController);
282 }
283 }
284
285 @override
286 void didChangeDependencies() {
287 super.didChangeDependencies();
288
289 if (_updateColors()) {
290 _updateAnimationControllers();
291 }
292 }
293
294 @override
295 void didUpdateWidget(CupertinoSegmentedControl<T> oldWidget) {
296 super.didUpdateWidget(oldWidget);
297
298 if (_updateColors() || oldWidget.children.length != widget.children.length) {
299 _updateAnimationControllers();
300 }
301
302 if (oldWidget.groupValue != widget.groupValue) {
303 int index = 0;
304 for (final T key in widget.children.keys) {
305 if (widget.groupValue == key) {
306 _childTweens[index] = _forwardBackgroundColorTween;
307 _selectionControllers[index].forward();
308 } else {
309 _childTweens[index] = _reverseBackgroundColorTween;
310 _selectionControllers[index].reverse();
311 }
312 index += 1;
313 }
314 }
315 }
316
317 @override
318 void dispose() {
319 for (final AnimationController animationController in _selectionControllers) {
320 animationController.dispose();
321 }
322 super.dispose();
323 }
324
325 void _onTapDown(T currentKey) {
326 if (_pressedKey == null && currentKey != widget.groupValue) {
327 setState(() {
328 _pressedKey = currentKey;
329 });
330 }
331 }
332
333 void _onTapCancel() {
334 setState(() {
335 _pressedKey = null;
336 });
337 }
338
339 void _onTap(T currentKey) {
340 if (currentKey != _pressedKey) {
341 return;
342 }
343 if (!widget.disabledChildren.contains(currentKey)) {
344 if (currentKey != widget.groupValue) {
345 widget.onValueChanged(currentKey);
346 }
347 }
348 _pressedKey = null;
349 }
350
351 Color? getTextColor(int index, T currentKey) {
352 if (widget.disabledChildren.contains(currentKey)) {
353 return _disabledTextColor;
354 }
355 if (_selectionControllers[index].isAnimating) {
356 return _textColorTween.evaluate(_selectionControllers[index]);
357 }
358 if (widget.groupValue == currentKey) {
359 return _unselectedColor;
360 }
361 return _selectedColor;
362 }
363
364 Color? getBackgroundColor(int index, T currentKey) {
365 if (widget.disabledChildren.contains(currentKey)) {
366 return widget.groupValue == currentKey ? _selectedDisabledColor : _unselectedDisabledColor;
367 }
368 if (_selectionControllers[index].isAnimating) {
369 return _childTweens[index].evaluate(_selectionControllers[index]);
370 }
371 if (widget.groupValue == currentKey) {
372 return _selectedColor;
373 }
374 if (_pressedKey == currentKey) {
375 return _pressedColor;
376 }
377 return _unselectedColor;
378 }
379
380 @override
381 Widget build(BuildContext context) {
382 final List<Widget> gestureChildren = <Widget>[];
383 final List<Color> backgroundColors = <Color>[];
384 int index = 0;
385 int? selectedIndex;
386 int? pressedIndex;
387 for (final T currentKey in widget.children.keys) {
388 selectedIndex = (widget.groupValue == currentKey) ? index : selectedIndex;
389 pressedIndex = (_pressedKey == currentKey) ? index : pressedIndex;
390
391 final TextStyle textStyle = DefaultTextStyle.of(
392 context,
393 ).style.copyWith(color: getTextColor(index, currentKey));
394 final IconThemeData iconTheme = IconThemeData(color: getTextColor(index, currentKey));
395
396 Widget child = Center(child: widget.children[currentKey]);
397
398 child = MouseRegion(
399 cursor: kIsWeb ? SystemMouseCursors.click : MouseCursor.defer,
400 child: GestureDetector(
401 behavior: HitTestBehavior.opaque,
402 onTapDown: widget.disabledChildren.contains(currentKey)
403 ? null
404 : (TapDownDetails event) {
405 _onTapDown(currentKey);
406 },
407 onTapCancel: widget.disabledChildren.contains(currentKey) ? null : _onTapCancel,
408 onTap: () {
409 _onTap(currentKey);
410 },
411 child: IconTheme(
412 data: iconTheme,
413 child: DefaultTextStyle(
414 style: textStyle,
415 child: Semantics(
416 button: true,
417 inMutuallyExclusiveGroup: true,
418 selected: widget.groupValue == currentKey,
419 child: child,
420 ),
421 ),
422 ),
423 ),
424 );
425
426 backgroundColors.add(getBackgroundColor(index, currentKey)!);
427 gestureChildren.add(child);
428 index += 1;
429 }
430
431 final Widget box = _SegmentedControlRenderWidget<T>(
432 selectedIndex: selectedIndex,
433 pressedIndex: pressedIndex,
434 backgroundColors: backgroundColors,
435 borderColor: _borderColor!,
436 children: gestureChildren,
437 );
438
439 return Padding(
440 padding: widget.padding ?? _kHorizontalItemPadding,
441 child: UnconstrainedBox(constrainedAxis: Axis.horizontal, child: box),
442 );
443 }
444}
445
446class _SegmentedControlRenderWidget<T> extends MultiChildRenderObjectWidget {
447 const _SegmentedControlRenderWidget({
448 super.key,
449 super.children,
450 required this.selectedIndex,
451 required this.pressedIndex,
452 required this.backgroundColors,
453 required this.borderColor,
454 });
455
456 final int? selectedIndex;
457 final int? pressedIndex;
458 final List<Color> backgroundColors;
459 final Color borderColor;
460
461 @override
462 RenderObject createRenderObject(BuildContext context) {
463 return _RenderSegmentedControl<T>(
464 textDirection: Directionality.of(context),
465 selectedIndex: selectedIndex,
466 pressedIndex: pressedIndex,
467 backgroundColors: backgroundColors,
468 borderColor: borderColor,
469 );
470 }
471
472 @override
473 void updateRenderObject(BuildContext context, _RenderSegmentedControl<T> renderObject) {
474 renderObject
475 ..textDirection = Directionality.of(context)
476 ..selectedIndex = selectedIndex
477 ..pressedIndex = pressedIndex
478 ..backgroundColors = backgroundColors
479 ..borderColor = borderColor;
480 }
481}
482
483class _SegmentedControlContainerBoxParentData extends ContainerBoxParentData<RenderBox> {
484 RSuperellipse? surroundingRect;
485}
486
487typedef _NextChild = RenderBox? Function(RenderBox child);
488
489class _RenderSegmentedControl<T> extends RenderBox
490 with
491 ContainerRenderObjectMixin<RenderBox, ContainerBoxParentData<RenderBox>>,
492 RenderBoxContainerDefaultsMixin<RenderBox, ContainerBoxParentData<RenderBox>> {
493 _RenderSegmentedControl({
494 required int? selectedIndex,
495 required int? pressedIndex,
496 required TextDirection textDirection,
497 required List<Color> backgroundColors,
498 required Color borderColor,
499 }) : _textDirection = textDirection,
500 _selectedIndex = selectedIndex,
501 _pressedIndex = pressedIndex,
502 _backgroundColors = backgroundColors,
503 _borderColor = borderColor;
504
505 int? get selectedIndex => _selectedIndex;
506 int? _selectedIndex;
507 set selectedIndex(int? value) {
508 if (_selectedIndex == value) {
509 return;
510 }
511 _selectedIndex = value;
512 markNeedsPaint();
513 }
514
515 int? get pressedIndex => _pressedIndex;
516 int? _pressedIndex;
517 set pressedIndex(int? value) {
518 if (_pressedIndex == value) {
519 return;
520 }
521 _pressedIndex = value;
522 markNeedsPaint();
523 }
524
525 TextDirection get textDirection => _textDirection;
526 TextDirection _textDirection;
527 set textDirection(TextDirection value) {
528 if (_textDirection == value) {
529 return;
530 }
531 _textDirection = value;
532 markNeedsLayout();
533 }
534
535 List<Color> get backgroundColors => _backgroundColors;
536 List<Color> _backgroundColors;
537 set backgroundColors(List<Color> value) {
538 if (_backgroundColors == value) {
539 return;
540 }
541 _backgroundColors = value;
542 markNeedsPaint();
543 }
544
545 Color get borderColor => _borderColor;
546 Color _borderColor;
547 set borderColor(Color value) {
548 if (_borderColor == value) {
549 return;
550 }
551 _borderColor = value;
552 markNeedsPaint();
553 }
554
555 @override
556 double computeMinIntrinsicWidth(double height) {
557 RenderBox? child = firstChild;
558 double minWidth = 0.0;
559 while (child != null) {
560 final _SegmentedControlContainerBoxParentData childParentData =
561 child.parentData! as _SegmentedControlContainerBoxParentData;
562 final double childWidth = child.getMinIntrinsicWidth(height);
563 minWidth = math.max(minWidth, childWidth);
564 child = childParentData.nextSibling;
565 }
566 return minWidth * childCount;
567 }
568
569 @override
570 double computeMaxIntrinsicWidth(double height) {
571 RenderBox? child = firstChild;
572 double maxWidth = 0.0;
573 while (child != null) {
574 final _SegmentedControlContainerBoxParentData childParentData =
575 child.parentData! as _SegmentedControlContainerBoxParentData;
576 final double childWidth = child.getMaxIntrinsicWidth(height);
577 maxWidth = math.max(maxWidth, childWidth);
578 child = childParentData.nextSibling;
579 }
580 return maxWidth * childCount;
581 }
582
583 @override
584 double computeMinIntrinsicHeight(double width) {
585 RenderBox? child = firstChild;
586 double minHeight = 0.0;
587 while (child != null) {
588 final _SegmentedControlContainerBoxParentData childParentData =
589 child.parentData! as _SegmentedControlContainerBoxParentData;
590 final double childHeight = child.getMinIntrinsicHeight(width);
591 minHeight = math.max(minHeight, childHeight);
592 child = childParentData.nextSibling;
593 }
594 return minHeight;
595 }
596
597 @override
598 double computeMaxIntrinsicHeight(double width) {
599 RenderBox? child = firstChild;
600 double maxHeight = 0.0;
601 while (child != null) {
602 final _SegmentedControlContainerBoxParentData childParentData =
603 child.parentData! as _SegmentedControlContainerBoxParentData;
604 final double childHeight = child.getMaxIntrinsicHeight(width);
605 maxHeight = math.max(maxHeight, childHeight);
606 child = childParentData.nextSibling;
607 }
608 return maxHeight;
609 }
610
611 @override
612 double? computeDistanceToActualBaseline(TextBaseline baseline) {
613 return defaultComputeDistanceToHighestActualBaseline(baseline);
614 }
615
616 @override
617 void setupParentData(RenderBox child) {
618 if (child.parentData is! _SegmentedControlContainerBoxParentData) {
619 child.parentData = _SegmentedControlContainerBoxParentData();
620 }
621 }
622
623 void _layoutRects(_NextChild nextChild, RenderBox? leftChild, RenderBox? rightChild) {
624 RenderBox? child = leftChild;
625 double start = 0.0;
626 while (child != null) {
627 final _SegmentedControlContainerBoxParentData childParentData =
628 child.parentData! as _SegmentedControlContainerBoxParentData;
629 final Offset childOffset = Offset(start, 0.0);
630 childParentData.offset = childOffset;
631 final Rect childRect = Rect.fromLTWH(start, 0.0, child.size.width, child.size.height);
632 final RSuperellipse rChildRect;
633 if (child == leftChild) {
634 rChildRect = RSuperellipse.fromRectAndCorners(
635 childRect,
636 topLeft: const Radius.circular(3.0),
637 bottomLeft: const Radius.circular(3.0),
638 );
639 } else if (child == rightChild) {
640 rChildRect = RSuperellipse.fromRectAndCorners(
641 childRect,
642 topRight: const Radius.circular(3.0),
643 bottomRight: const Radius.circular(3.0),
644 );
645 } else {
646 rChildRect = RSuperellipse.fromRectAndCorners(childRect);
647 }
648 childParentData.surroundingRect = rChildRect;
649 start += child.size.width;
650 child = nextChild(child);
651 }
652 }
653
654 Size _calculateChildSize(BoxConstraints constraints) {
655 double maxHeight = _kMinSegmentedControlHeight;
656 double childWidth = constraints.minWidth / childCount;
657 RenderBox? child = firstChild;
658 while (child != null) {
659 childWidth = math.max(childWidth, child.getMaxIntrinsicWidth(double.infinity));
660 child = childAfter(child);
661 }
662 childWidth = math.min(childWidth, constraints.maxWidth / childCount);
663 child = firstChild;
664 while (child != null) {
665 final double boxHeight = child.getMaxIntrinsicHeight(childWidth);
666 maxHeight = math.max(maxHeight, boxHeight);
667 child = childAfter(child);
668 }
669 return Size(childWidth, maxHeight);
670 }
671
672 Size _computeOverallSizeFromChildSize(Size childSize) {
673 return constraints.constrain(Size(childSize.width * childCount, childSize.height));
674 }
675
676 @override
677 double? computeDryBaseline(covariant BoxConstraints constraints, TextBaseline baseline) {
678 final Size childSize = _calculateChildSize(constraints);
679 final BoxConstraints childConstraints = BoxConstraints.tight(childSize);
680
681 BaselineOffset baselineOffset = BaselineOffset.noBaseline;
682 for (RenderBox? child = firstChild; child != null; child = childAfter(child)) {
683 baselineOffset = baselineOffset.minOf(
684 BaselineOffset(child.getDryBaseline(childConstraints, baseline)),
685 );
686 }
687 return baselineOffset.offset;
688 }
689
690 @override
691 Size computeDryLayout(BoxConstraints constraints) {
692 final Size childSize = _calculateChildSize(constraints);
693 return _computeOverallSizeFromChildSize(childSize);
694 }
695
696 @override
697 void performLayout() {
698 final BoxConstraints constraints = this.constraints;
699 final Size childSize = _calculateChildSize(constraints);
700
701 final BoxConstraints childConstraints = BoxConstraints.tightFor(
702 width: childSize.width,
703 height: childSize.height,
704 );
705
706 RenderBox? child = firstChild;
707 while (child != null) {
708 child.layout(childConstraints, parentUsesSize: true);
709 child = childAfter(child);
710 }
711
712 switch (textDirection) {
713 case TextDirection.rtl:
714 _layoutRects(childBefore, lastChild, firstChild);
715 case TextDirection.ltr:
716 _layoutRects(childAfter, firstChild, lastChild);
717 }
718
719 size = _computeOverallSizeFromChildSize(childSize);
720 }
721
722 @override
723 void paint(PaintingContext context, Offset offset) {
724 RenderBox? child = firstChild;
725 int index = 0;
726 while (child != null) {
727 _paintChild(context, offset, child, index);
728 child = childAfter(child);
729 index += 1;
730 }
731 }
732
733 void _paintChild(PaintingContext context, Offset offset, RenderBox child, int childIndex) {
734 final _SegmentedControlContainerBoxParentData childParentData =
735 child.parentData! as _SegmentedControlContainerBoxParentData;
736
737 context.canvas.drawRSuperellipse(
738 childParentData.surroundingRect!.shift(offset),
739 Paint()
740 ..color = backgroundColors[childIndex]
741 ..style = PaintingStyle.fill,
742 );
743 context.canvas.drawRSuperellipse(
744 childParentData.surroundingRect!.shift(offset),
745 Paint()
746 ..color = borderColor
747 ..strokeWidth = 1.0
748 ..style = PaintingStyle.stroke,
749 );
750
751 context.paintChild(child, childParentData.offset + offset);
752 }
753
754 @override
755 bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
756 RenderBox? child = lastChild;
757 while (child != null) {
758 final _SegmentedControlContainerBoxParentData childParentData =
759 child.parentData! as _SegmentedControlContainerBoxParentData;
760 if (childParentData.surroundingRect!.outerRect.contains(position)) {
761 return result.addWithPaintOffset(
762 offset: childParentData.offset,
763 position: position,
764 hitTest: (BoxHitTestResult result, Offset localOffset) {
765 assert(localOffset == position - childParentData.offset);
766 return child!.hitTest(result, position: localOffset);
767 },
768 );
769 }
770 child = childParentData.previousSibling;
771 }
772 return false;
773 }
774}
775