Files
MWClash/lib/widgets/tab.dart
chen08209 676f2d058a Add windows server mode start process verify
Add linux deb dependencies

Add backup recovery strategy select

Support custom text scaling

Optimize the display of different text scale

Optimize windows setup experience

Optimize startTun performance

Optimize android tv experience

Optimize default option

Optimize computed text size

Optimize hyperOS freeform window

Add developer mode

Update core

Optimize more details
2025-05-01 00:02:29 +08:00

1101 lines
33 KiB
Dart

import 'dart:math' as math;
import 'dart:math';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/physics.dart';
import 'package:flutter/rendering.dart';
const EdgeInsetsGeometry _kHorizontalItemPadding =
EdgeInsets.symmetric(vertical: 2, horizontal: 3);
const Radius _kCornerRadius = Radius.circular(9);
const Radius _kThumbRadius = Radius.circular(8);
const EdgeInsets _kThumbInsets = EdgeInsets.symmetric(horizontal: 1);
const double _kMinSegmentedControlHeight = 28.0;
const EdgeInsets _kSeparatorInset = EdgeInsets.symmetric(vertical: 5);
const double _kSeparatorWidth = 1;
const double _kMinThumbScale = 0.95;
const double _kSegmentMinPadding = 10;
const double _kTouchYDistanceThreshold = 50.0 * 50.0;
const double _kContentPressedMinOpacity = 0.2;
const double _kFontSize = 13.0;
const FontWeight _kFontWeight = FontWeight.w500;
const FontWeight _kHighlightedFontWeight = FontWeight.w600;
const Color _kDisabledContentColor = Color.fromARGB(115, 122, 122, 122);
final SpringSimulation _kThumbSpringAnimationSimulation = SpringSimulation(
const SpringDescription(mass: 1, stiffness: 503.551, damping: 44.8799),
0,
1,
0,
);
const Duration _kSpringAnimationDuration = Duration(milliseconds: 412);
const Duration _kOpacityAnimationDuration = Duration(milliseconds: 470);
const Duration _kHighlightAnimationDuration = Duration(milliseconds: 200);
class CommonTabBar<T extends Object> extends StatefulWidget {
CommonTabBar({
super.key,
required this.children,
required this.onValueChanged,
this.disabledChildren = const <Never>{},
this.groupValue,
required this.thumbColor,
this.padding = _kHorizontalItemPadding,
this.backgroundColor,
this.proportionalWidth = false,
}) : assert(children.length >= 2),
assert(
groupValue == null || children.keys.contains(groupValue),
'The groupValue must be either null or one of the keys in the children map.',
);
final Map<T, Widget> children;
final Set<T> disabledChildren;
final T? groupValue;
final ValueChanged<T?> onValueChanged;
final Color? backgroundColor;
final Color thumbColor;
final bool proportionalWidth;
final EdgeInsetsGeometry padding;
@override
State<CommonTabBar<T>> createState() => _CommonTabBarState<T>();
}
class _CommonTabBarState<T extends Object> extends State<CommonTabBar<T>>
with TickerProviderStateMixin<CommonTabBar<T>> {
late final AnimationController thumbController = AnimationController(
duration: _kSpringAnimationDuration,
value: 0,
vsync: this,
);
Animatable<Rect?>? thumbAnimatable;
late final AnimationController thumbScaleController = AnimationController(
duration: _kSpringAnimationDuration,
value: 0,
vsync: this,
);
late Animation<double> thumbScaleAnimation = thumbScaleController.drive(
Tween<double>(begin: 1, end: _kMinThumbScale),
);
final TapGestureRecognizer tap = TapGestureRecognizer();
final HorizontalDragGestureRecognizer drag =
HorizontalDragGestureRecognizer();
final LongPressGestureRecognizer longPress = LongPressGestureRecognizer();
final GlobalKey segmentedControlRenderWidgetKey = GlobalKey();
@override
void initState() {
super.initState();
final GestureArenaTeam team = GestureArenaTeam();
longPress.team = team;
drag.team = team;
team.captain = drag;
drag
..onDown = onDown
..onUpdate = onUpdate
..onEnd = onEnd
..onCancel = onCancel;
tap.onTapUp = onTapUp;
longPress.onLongPress = () {};
highlighted = widget.groupValue;
}
@override
void didUpdateWidget(CommonTabBar<T> oldWidget) {
super.didUpdateWidget(oldWidget);
if (!isThumbDragging && highlighted != widget.groupValue) {
thumbController.animateWith(_kThumbSpringAnimationSimulation);
thumbAnimatable = null;
highlighted = widget.groupValue;
}
}
@override
void dispose() {
thumbScaleController.dispose();
thumbController.dispose();
drag.dispose();
tap.dispose();
longPress.dispose();
super.dispose();
}
bool? _startedOnSelectedSegment;
bool _startedOnDisabledSegment = false;
bool get isThumbDragging =>
(_startedOnSelectedSegment ?? false) && !_startedOnDisabledSegment;
T segmentForXPosition(double dx) {
final BuildContext currentContext =
segmentedControlRenderWidgetKey.currentContext!;
final _RenderSegmentedControl<T> renderBox =
currentContext.findRenderObject()! as _RenderSegmentedControl<T>;
final int numOfChildren = widget.children.length;
assert(renderBox.hasSize);
assert(numOfChildren >= 2);
int segmentIndex = renderBox.getClosestSegmentIndex(dx);
switch (Directionality.of(context)) {
case TextDirection.ltr:
break;
case TextDirection.rtl:
segmentIndex = numOfChildren - 1 - segmentIndex;
}
return widget.children.keys.elementAt(segmentIndex);
}
bool _hasDraggedTooFar(DragUpdateDetails details) {
final RenderBox renderBox = context.findRenderObject()! as RenderBox;
assert(renderBox.hasSize);
final Size size = renderBox.size;
final Offset offCenter =
details.localPosition - Offset(size.width / 2, size.height / 2);
final double l2 =
math.pow(math.max(0.0, offCenter.dx.abs() - size.width / 2), 2) +
math.pow(math.max(0.0, offCenter.dy.abs() - size.height / 2), 2)
as double;
return l2 > _kTouchYDistanceThreshold;
}
void _playThumbScaleAnimation({required bool isExpanding}) {
thumbScaleAnimation = thumbScaleController.drive(
Tween<double>(
begin: thumbScaleAnimation.value,
end: isExpanding ? 1 : _kMinThumbScale),
);
thumbScaleController.animateWith(_kThumbSpringAnimationSimulation);
}
void onHighlightChangedByGesture(T newValue) {
if (highlighted == newValue) {
return;
}
setState(() {
highlighted = newValue;
});
thumbController.animateWith(_kThumbSpringAnimationSimulation);
thumbAnimatable = null;
}
void onPressedChangedByGesture(T? newValue) {
if (pressed != newValue) {
setState(() {
pressed = newValue;
});
}
}
void onTapUp(TapUpDetails details) {
if (isThumbDragging) {
return;
}
final T segment = segmentForXPosition(details.localPosition.dx);
onPressedChangedByGesture(null);
if (segment != widget.groupValue &&
!widget.disabledChildren.contains(segment)) {
widget.onValueChanged(segment);
}
}
void onDown(DragDownDetails details) {
final T touchDownSegment = segmentForXPosition(details.localPosition.dx);
_startedOnSelectedSegment = touchDownSegment == highlighted;
_startedOnDisabledSegment =
widget.disabledChildren.contains(touchDownSegment);
if (widget.disabledChildren.contains(touchDownSegment)) {
return;
}
onPressedChangedByGesture(touchDownSegment);
if (isThumbDragging) {
_playThumbScaleAnimation(isExpanding: false);
}
}
void onUpdate(DragUpdateDetails details) {
if (_startedOnDisabledSegment) {
return;
}
final T touchDownSegment = segmentForXPosition(details.localPosition.dx);
if (widget.disabledChildren.contains(touchDownSegment)) {
return;
}
if (isThumbDragging) {
onPressedChangedByGesture(touchDownSegment);
onHighlightChangedByGesture(touchDownSegment);
} else {
final T? segment = _hasDraggedTooFar(details)
? null
: segmentForXPosition(details.localPosition.dx);
onPressedChangedByGesture(segment);
}
}
void onEnd(DragEndDetails details) {
final T? pressed = this.pressed;
if (isThumbDragging) {
_playThumbScaleAnimation(isExpanding: true);
if (highlighted != widget.groupValue) {
widget.onValueChanged(highlighted);
}
} else if (pressed != null) {
onHighlightChangedByGesture(pressed);
assert(pressed == highlighted);
if (highlighted != widget.groupValue) {
widget.onValueChanged(highlighted);
}
}
onPressedChangedByGesture(null);
_startedOnSelectedSegment = null;
}
void onCancel() {
if (isThumbDragging) {
_playThumbScaleAnimation(isExpanding: true);
}
onPressedChangedByGesture(null);
_startedOnSelectedSegment = null;
}
T? highlighted;
T? pressed;
@override
Widget build(BuildContext context) {
assert(widget.children.length >= 2);
List<Widget> children = <Widget>[];
bool isPreviousSegmentHighlighted = false;
int index = 0;
int? highlightedIndex;
for (final MapEntry<T, Widget> entry in widget.children.entries) {
final bool isHighlighted = highlighted == entry.key;
if (isHighlighted) {
highlightedIndex = index;
}
if (index != 0) {
children.add(
_SegmentSeparator(
key: ValueKey<int>(index),
highlighted: isPreviousSegmentHighlighted || isHighlighted,
),
);
}
final TextDirection textDirection = Directionality.of(context);
final _SegmentLocation segmentLocation = switch (textDirection) {
TextDirection.ltr when index == 0 => _SegmentLocation.leftmost,
TextDirection.ltr when index == widget.children.length - 1 =>
_SegmentLocation.rightmost,
TextDirection.rtl when index == widget.children.length - 1 =>
_SegmentLocation.leftmost,
TextDirection.rtl when index == 0 => _SegmentLocation.rightmost,
TextDirection.ltr || TextDirection.rtl => _SegmentLocation.inbetween,
};
children.add(
Semantics(
button: true,
onTap: () {
if (widget.disabledChildren.contains(entry.key)) {
return;
}
widget.onValueChanged(entry.key);
},
inMutuallyExclusiveGroup: true,
selected: widget.groupValue == entry.key,
child: MouseRegion(
cursor: kIsWeb ? SystemMouseCursors.click : MouseCursor.defer,
child: _Segment<T>(
key: ValueKey<T>(entry.key),
highlighted: isHighlighted,
pressed: pressed == entry.key,
isDragging: isThumbDragging,
enabled: !widget.disabledChildren.contains(entry.key),
segmentLocation: segmentLocation,
child: entry.value,
),
),
),
);
index += 1;
isPreviousSegmentHighlighted = isHighlighted;
}
assert((highlightedIndex == null) == (highlighted == null));
switch (Directionality.of(context)) {
case TextDirection.ltr:
break;
case TextDirection.rtl:
children = children.reversed.toList(growable: false);
if (highlightedIndex != null) {
highlightedIndex = index - 1 - highlightedIndex;
}
}
return UnconstrainedBox(
constrainedAxis: Axis.horizontal,
child: Container(
clipBehavior: Clip.antiAlias,
padding: widget.padding.resolve(Directionality.of(context)),
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(_kCornerRadius),
color: widget.backgroundColor,
),
child: AnimatedBuilder(
animation: thumbScaleAnimation,
builder: (BuildContext context, Widget? child) {
return _CommonTabBarRenderWidget<T>(
proportionalWidth: widget.proportionalWidth,
key: segmentedControlRenderWidgetKey,
highlightedIndex: highlightedIndex,
thumbColor: widget.thumbColor,
thumbScale: thumbScaleAnimation.value,
state: this,
children: children,
);
},
),
),
);
}
}
class _Segment<T> extends StatefulWidget {
const _Segment({
required ValueKey<T> key,
required this.child,
required this.pressed,
required this.highlighted,
required this.isDragging,
required this.enabled,
required this.segmentLocation,
}) : super(key: key);
final Widget child;
final bool pressed;
final bool highlighted;
final bool enabled;
final _SegmentLocation segmentLocation;
final bool isDragging;
bool get shouldFadeoutContent => pressed && !highlighted && enabled;
bool get shouldScaleContent =>
pressed && highlighted && isDragging && enabled;
@override
_SegmentState<T> createState() => _SegmentState<T>();
}
class _SegmentState<T> extends State<_Segment<T>>
with TickerProviderStateMixin<_Segment<T>> {
late final AnimationController highlightPressScaleController;
late Animation<double> highlightPressScaleAnimation;
@override
void initState() {
super.initState();
highlightPressScaleController = AnimationController(
duration: _kOpacityAnimationDuration,
value: widget.shouldScaleContent ? 1 : 0,
vsync: this,
);
highlightPressScaleAnimation = highlightPressScaleController.drive(
Tween<double>(begin: 1.0, end: _kMinThumbScale),
);
}
@override
void didUpdateWidget(_Segment<T> oldWidget) {
super.didUpdateWidget(oldWidget);
assert(oldWidget.key == widget.key);
if (oldWidget.shouldScaleContent != widget.shouldScaleContent) {
highlightPressScaleAnimation = highlightPressScaleController.drive(
Tween<double>(
begin: highlightPressScaleAnimation.value,
end: widget.shouldScaleContent ? _kMinThumbScale : 1.0,
),
);
highlightPressScaleController
.animateWith(_kThumbSpringAnimationSimulation);
}
}
@override
void dispose() {
highlightPressScaleController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final Alignment scaleAlignment = switch (widget.segmentLocation) {
_SegmentLocation.leftmost => Alignment.centerLeft,
_SegmentLocation.rightmost => Alignment.centerRight,
_SegmentLocation.inbetween => Alignment.center,
};
return MetaData(
behavior: HitTestBehavior.opaque,
child: IndexedStack(
alignment: Alignment.center,
children: <Widget>[
AnimatedOpacity(
opacity:
widget.shouldFadeoutContent ? _kContentPressedMinOpacity : 1,
duration: _kOpacityAnimationDuration,
curve: Curves.ease,
child: AnimatedDefaultTextStyle(
style: DefaultTextStyle.of(context).style.merge(
TextStyle(
fontWeight: widget.highlighted
? _kHighlightedFontWeight
: _kFontWeight,
fontSize: _kFontSize,
color: widget.enabled ? null : _kDisabledContentColor,
),
),
duration: _kHighlightAnimationDuration,
curve: Curves.ease,
child: ScaleTransition(
alignment: scaleAlignment,
scale: highlightPressScaleAnimation,
child: widget.child,
),
),
),
DefaultTextStyle.merge(
style: const TextStyle(
fontWeight: _kHighlightedFontWeight, fontSize: _kFontSize),
child: widget.child,
),
],
),
);
}
}
class _SegmentSeparator extends StatefulWidget {
const _SegmentSeparator({
required ValueKey<int> key,
required this.highlighted,
}) : super(key: key);
final bool highlighted;
@override
_SegmentSeparatorState createState() => _SegmentSeparatorState();
}
class _SegmentSeparatorState extends State<_SegmentSeparator>
with TickerProviderStateMixin<_SegmentSeparator> {
late final AnimationController separatorOpacityController;
@override
void initState() {
super.initState();
separatorOpacityController = AnimationController(
duration: _kSpringAnimationDuration,
value: widget.highlighted ? 0 : 1,
vsync: this,
);
}
@override
void didUpdateWidget(_SegmentSeparator oldWidget) {
super.didUpdateWidget(oldWidget);
assert(oldWidget.key == widget.key);
if (oldWidget.highlighted != widget.highlighted) {
separatorOpacityController.animateTo(
widget.highlighted ? 0 : 1,
duration: _kSpringAnimationDuration,
curve: Curves.ease,
);
}
}
@override
void dispose() {
separatorOpacityController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: separatorOpacityController,
child: const SizedBox(width: _kSeparatorWidth),
builder: (BuildContext context, Widget? child) {
return Padding(
padding: _kSeparatorInset,
child: DecoratedBox(
decoration: BoxDecoration(
color: Colors.transparent,
),
child: child,
),
);
},
);
}
}
class _CommonTabBarRenderWidget<T extends Object>
extends MultiChildRenderObjectWidget {
const _CommonTabBarRenderWidget({
super.key,
super.children,
required this.highlightedIndex,
required this.thumbColor,
required this.thumbScale,
required this.state,
required this.proportionalWidth,
});
final int? highlightedIndex;
final Color thumbColor;
final double thumbScale;
final bool proportionalWidth;
final _CommonTabBarState<T> state;
@override
RenderObject createRenderObject(BuildContext context) {
return _RenderSegmentedControl<T>(
highlightedIndex: highlightedIndex,
thumbColor: thumbColor,
thumbScale: thumbScale,
proportionalWidth: proportionalWidth,
state: state,
);
}
@override
void updateRenderObject(
BuildContext context, _RenderSegmentedControl<T> renderObject) {
assert(renderObject.state == state);
renderObject
..thumbColor = thumbColor
..thumbScale = thumbScale
..highlightedIndex = highlightedIndex
..proportionalWidth = proportionalWidth;
}
}
class _SegmentedControlContainerBoxParentData
extends ContainerBoxParentData<RenderBox> {}
enum _SegmentLocation { leftmost, rightmost, inbetween }
class _RenderSegmentedControl<T extends Object> extends RenderBox
with
ContainerRenderObjectMixin<RenderBox,
ContainerBoxParentData<RenderBox>>,
RenderBoxContainerDefaultsMixin<RenderBox,
ContainerBoxParentData<RenderBox>> {
_RenderSegmentedControl({
required int? highlightedIndex,
required Color thumbColor,
required double thumbScale,
required bool proportionalWidth,
required this.state,
}) : _highlightedIndex = highlightedIndex,
_thumbColor = thumbColor,
_thumbScale = thumbScale,
_proportionalWidth = proportionalWidth;
final _CommonTabBarState<T> state;
Rect? currentThumbRect;
@override
void attach(PipelineOwner owner) {
super.attach(owner);
state.thumbController.addListener(markNeedsPaint);
}
@override
void detach() {
state.thumbController.removeListener(markNeedsPaint);
super.detach();
}
double get thumbScale => _thumbScale;
double _thumbScale;
set thumbScale(double value) {
if (_thumbScale == value) {
return;
}
_thumbScale = value;
if (state.highlighted != null) {
markNeedsPaint();
}
}
int? get highlightedIndex => _highlightedIndex;
int? _highlightedIndex;
set highlightedIndex(int? value) {
if (_highlightedIndex == value) {
return;
}
_highlightedIndex = value;
markNeedsPaint();
}
Color get thumbColor => _thumbColor;
Color _thumbColor;
set thumbColor(Color value) {
if (_thumbColor == value) {
return;
}
_thumbColor = value;
markNeedsPaint();
}
bool get proportionalWidth => _proportionalWidth;
bool _proportionalWidth;
set proportionalWidth(bool value) {
if (_proportionalWidth == value) {
return;
}
_proportionalWidth = value;
markNeedsLayout();
}
@override
void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
assert(debugHandleEvent(event, entry));
if (event is PointerDownEvent && !state.isThumbDragging) {
state.tap.addPointer(event);
state.longPress.addPointer(event);
state.drag.addPointer(event);
}
}
double get separatorWidth => _kSeparatorInset.horizontal + _kSeparatorWidth;
double get totalSeparatorWidth => separatorWidth * (childCount ~/ 2);
int getClosestSegmentIndex(double dx) {
int index = 0;
RenderBox? child = firstChild;
while (child != null) {
final _SegmentedControlContainerBoxParentData childParentData =
child.parentData! as _SegmentedControlContainerBoxParentData;
final double clampX = clampDouble(
dx,
childParentData.offset.dx,
child.size.width + childParentData.offset.dx,
);
if (dx <= clampX) {
break;
}
index++;
child = nonSeparatorChildAfter(child);
}
final int segmentCount = childCount ~/ 2 + 1;
return min(index, segmentCount - 1);
}
RenderBox? nonSeparatorChildAfter(RenderBox child) {
final RenderBox? nextChild = childAfter(child);
return nextChild == null ? null : childAfter(nextChild);
}
@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 * _kSegmentMinPadding) * childCount +
totalSeparatorWidth;
}
@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 + 2 * _kSegmentMinPadding) * childCount +
totalSeparatorWidth;
}
@override
double computeMinIntrinsicHeight(double width) {
RenderBox? child = firstChild;
double maxMinChildHeight = _kMinSegmentedControlHeight;
while (child != null) {
final double childHeight = child.getMinIntrinsicHeight(width);
maxMinChildHeight = math.max(maxMinChildHeight, childHeight);
child = nonSeparatorChildAfter(child);
}
return maxMinChildHeight;
}
@override
double computeMaxIntrinsicHeight(double width) {
RenderBox? child = firstChild;
double maxMaxChildHeight = _kMinSegmentedControlHeight;
while (child != null) {
final double childHeight = child.getMaxIntrinsicHeight(width);
maxMaxChildHeight = math.max(maxMaxChildHeight, childHeight);
child = nonSeparatorChildAfter(child);
}
return maxMaxChildHeight;
}
@override
double? computeDistanceToActualBaseline(TextBaseline baseline) {
return defaultComputeDistanceToHighestActualBaseline(baseline);
}
@override
void setupParentData(RenderBox child) {
if (child.parentData is! _SegmentedControlContainerBoxParentData) {
child.parentData = _SegmentedControlContainerBoxParentData();
}
}
double _getMaxChildHeight(BoxConstraints constraints, double childWidth) {
double maxHeight = _kMinSegmentedControlHeight;
RenderBox? child = firstChild;
while (child != null) {
final double boxHeight = child.getMaxIntrinsicHeight(childWidth);
maxHeight = math.max(maxHeight, boxHeight);
child = nonSeparatorChildAfter(child);
}
return maxHeight;
}
double _getMaxChildWidth(BoxConstraints constraints) {
final int childCount = this.childCount ~/ 2 + 1;
double childWidth =
(constraints.minWidth - totalSeparatorWidth) / childCount;
RenderBox? child = firstChild;
while (child != null) {
childWidth = math.max(
childWidth,
child.getMaxIntrinsicWidth(double.infinity) + 2 * _kSegmentMinPadding,
);
child = nonSeparatorChildAfter(child);
}
return math.min(
childWidth, (constraints.maxWidth - totalSeparatorWidth) / childCount);
}
List<double> _getChildWidths(BoxConstraints constraints) {
if (!proportionalWidth) {
final double maxChildWidth = _getMaxChildWidth(constraints);
final int segmentCount = childCount ~/ 2 + 1;
return List<double>.filled(segmentCount, maxChildWidth);
}
final List<double> segmentWidths = <double>[];
RenderBox? child = firstChild;
while (child != null) {
final double childWidth =
child.getMaxIntrinsicWidth(double.infinity) + 2 * _kSegmentMinPadding;
child = nonSeparatorChildAfter(child);
segmentWidths.add(childWidth);
}
final double totalWidth = segmentWidths.sum;
final double allowedMaxWidth = constraints.maxWidth - totalSeparatorWidth;
final double allowedMinWidth = constraints.minWidth - totalSeparatorWidth;
final double scale =
clampDouble(totalWidth, allowedMinWidth, allowedMaxWidth) / totalWidth;
if (scale != 1) {
for (int i = 0; i < segmentWidths.length; i++) {
segmentWidths[i] = segmentWidths[i] * scale;
}
}
return segmentWidths;
}
Size _computeOverallSize(BoxConstraints constraints) {
final double maxChildHeight =
_getMaxChildHeight(constraints, constraints.maxWidth);
return constraints.constrain(
Size(_getChildWidths(constraints).sum + totalSeparatorWidth,
maxChildHeight),
);
}
@override
double? computeDryBaseline(
covariant BoxConstraints constraints, TextBaseline baseline) {
final List<double> segmentWidths = _getChildWidths(constraints);
final double childHeight =
_getMaxChildHeight(constraints, constraints.maxWidth);
int index = 0;
BaselineOffset baselineOffset = BaselineOffset.noBaseline;
RenderBox? child = firstChild;
while (child != null) {
final BoxConstraints childConstraints = BoxConstraints.tight(
Size(segmentWidths[index], childHeight),
);
baselineOffset = baselineOffset.minOf(
BaselineOffset(child.getDryBaseline(childConstraints, baseline)),
);
child = nonSeparatorChildAfter(child);
index++;
}
return baselineOffset.offset;
}
@override
Size computeDryLayout(BoxConstraints constraints) {
return _computeOverallSize(constraints);
}
@override
void performLayout() {
final BoxConstraints constraints = this.constraints;
final List<double> segmentWidths = _getChildWidths(constraints);
final double childHeight = _getMaxChildHeight(constraints, double.infinity);
final BoxConstraints separatorConstraints = BoxConstraints(
minHeight: childHeight,
maxHeight: childHeight,
);
RenderBox? child = firstChild;
int index = 0;
double start = 0;
while (child != null) {
final BoxConstraints childConstraints = BoxConstraints.tight(
Size(segmentWidths[index ~/ 2], childHeight),
);
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;
assert(
index.isEven ||
child.size.width == _kSeparatorWidth + _kSeparatorInset.horizontal,
'${child.size.width} != ${_kSeparatorWidth + _kSeparatorInset.horizontal}',
);
child = childAfter(child);
index += 1;
}
size = _computeOverallSize(constraints);
}
Rect? moveThumbRectInBound(Rect? thumbRect, List<RenderBox> children) {
assert(hasSize);
assert(children.length >= 2);
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;
assert(rightMost > leftMost);
return Rect.fromLTRB(
math.max(thumbRect.left, leftMost - _kThumbInsets.left),
firstChildOffset.dy - _kThumbInsets.top,
math.min(thumbRect.right, rightMost + _kThumbInsets.right),
firstChildOffset.dy + children.first.size.height + _kThumbInsets.bottom,
);
}
@override
void paint(PaintingContext context, Offset offset) {
final List<RenderBox> children = getChildrenAsList();
for (int index = 1; index < childCount; index += 2) {
_paintSeparator(context, offset, children[index]);
}
final int? highlightedChildIndex = highlightedIndex;
if (highlightedChildIndex != null) {
final RenderBox selectedChild = children[highlightedChildIndex * 2];
final _SegmentedControlContainerBoxParentData childParentData =
selectedChild.parentData! as _SegmentedControlContainerBoxParentData;
final Rect newThumbRect = _kThumbInsets.inflateRect(
childParentData.offset & selectedChild.size,
);
if (state.thumbController.isAnimating) {
final Animatable<Rect?>? thumbTween = state.thumbAnimatable;
if (thumbTween == null) {
final Rect startingRect =
moveThumbRectInBound(currentThumbRect, children) ?? newThumbRect;
state.thumbAnimatable =
RectTween(begin: startingRect, end: newThumbRect);
} else if (newThumbRect != thumbTween.transform(1)) {
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 _SegmentLocation childLocation;
if (highlightedChildIndex == 0) {
childLocation = _SegmentLocation.leftmost;
} else if (highlightedChildIndex == children.length ~/ 2) {
childLocation = _SegmentLocation.rightmost;
} else {
childLocation = _SegmentLocation.inbetween;
}
final double delta = switch (childLocation) {
_SegmentLocation.leftmost =>
unscaledThumbRect.width - unscaledThumbRect.width * thumbScale,
_SegmentLocation.rightmost =>
unscaledThumbRect.width * thumbScale - unscaledThumbRect.width,
_SegmentLocation.inbetween => 0,
};
final Rect thumbRect = Rect.fromCenter(
center: unscaledThumbRect.center - Offset(delta / 2, 0),
width: unscaledThumbRect.width * thumbScale,
height: unscaledThumbRect.height * thumbScale,
);
_paintThumb(context, offset, thumbRect);
} else {
currentThumbRect = null;
}
for (int index = 0; index < children.length; index += 2) {
_paintChild(context, offset, children[index]);
}
}
final Paint separatorPaint = Paint();
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) {
// const List<BoxShadow> thumbShadow = <BoxShadow>[
// BoxShadow(color: Color(0x1F000000), offset: Offset(0, 3), blurRadius: 8),
// BoxShadow(color: Color(0x0A000000), offset: Offset(0, 3), blurRadius: 1),
// ];
final RRect thumbRRect =
RRect.fromRectAndRadius(thumbRect.shift(offset), _kThumbRadius);
// for (final BoxShadow shadow in thumbShadow) {
// context.canvas
// .drawRRect(thumbRRect.shift(shadow.offset), shadow.toPaint());
// }
context.canvas.drawRRect(
thumbRRect.inflate(0.5), Paint()..color = const Color(0x0A000000));
context.canvas.drawRRect(thumbRRect, Paint()..color = thumbColor);
}
@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) {
assert(localOffset == position - childParentData.offset);
return child!.hitTest(result, position: localOffset);
},
);
}
child = childParentData.previousSibling;
}
return false;
}
}