diff --git a/lib/common/widgets/flutter/draggable_sheet/draggable_scrollable_sheet_dyn.dart b/lib/common/widgets/flutter/draggable_sheet/draggable_scrollable_sheet_dyn.dart index 59585f9ef..25f3c6a60 100644 --- a/lib/common/widgets/flutter/draggable_sheet/draggable_scrollable_sheet_dyn.dart +++ b/lib/common/widgets/flutter/draggable_sheet/draggable_scrollable_sheet_dyn.dart @@ -20,7 +20,7 @@ import 'dart:math' as math; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart' hide DraggableScrollableSheet; /// Controls a [DraggableScrollableSheet]. /// @@ -112,11 +112,10 @@ class DraggableScrollableController extends ChangeNotifier { _assertAttached(); assert(size >= 0 && size <= 1); assert(duration != Duration.zero); - final AnimationController animationController = - AnimationController.unbounded( - vsync: _attachedController!.position.context.vsync, - value: _attachedController!.extent.currentSize, - ); + final animationController = AnimationController.unbounded( + vsync: _attachedController!.position.context.vsync, + value: _attachedController!.extent.currentSize, + ); _animationControllers.add(animationController); _attachedController!.position.goIdle(); // This disables any snapping until the next user interaction with the sheet. @@ -583,7 +582,7 @@ class _DraggableScrollableSheetState extends State { } List _impliedSnapSizes() { - for (int index = 0; index < (widget.snapSizes?.length ?? 0); index += 1) { + for (var index = 0; index < (widget.snapSizes?.length ?? 0); index += 1) { final double snapSize = widget.snapSizes![index]; assert( snapSize >= widget.minChildSize && snapSize <= widget.maxChildSize, @@ -684,11 +683,11 @@ class _DraggableScrollableSheetState extends State { // have changed when the widget was updated. WidgetsBinding.instance.addPostFrameCallback((Duration timeStamp) { for ( - int index = 0; + var index = 0; index < _scrollController.positions.length; index++ ) { - final _DraggableScrollableSheetScrollPosition position = + final position = _scrollController.positions.elementAt(index) as _DraggableScrollableSheetScrollPosition; position.goBallistic(0); @@ -702,7 +701,7 @@ class _DraggableScrollableSheetState extends State { .asMap() .keys .map((int index) { - final String snapSizeString = widget.snapSizes![index].toString(); + final snapSizeString = widget.snapSizes![index].toString(); if (index == invalidIndex) { return '>>> $snapSizeString <<<'; } @@ -917,14 +916,10 @@ class _DraggableScrollableSheetScrollPosition ); } - final AnimationController ballisticController = - AnimationController.unbounded( - debugLabel: objectRuntimeType( - this, - '_DraggableScrollableSheetPosition', - ), - vsync: context.vsync, - ); + final ballisticController = AnimationController.unbounded( + debugLabel: objectRuntimeType(this, '_DraggableScrollableSheetPosition'), + vsync: context.vsync, + ); _ballisticControllers.add(ballisticController); double lastPosition = extent.currentPixels; @@ -1080,8 +1075,7 @@ class _InheritedResetNotifier extends InheritedNotifier<_ResetNotifier> { return false; } assert(widget is _InheritedResetNotifier); - final _InheritedResetNotifier inheritedNotifier = - widget as _InheritedResetNotifier; + final inheritedNotifier = widget as _InheritedResetNotifier; final bool wasCalled = inheritedNotifier.notifier!._wasCalled; inheritedNotifier.notifier!._wasCalled = false; return wasCalled; @@ -1158,6 +1152,10 @@ class _SnappingSimulation extends Simulation { return pixelSnapSizes.first; } final double nextSize = pixelSnapSizes[indexOfNextSize]; + // If already snapped - keep this as target size + if (nextSize == position) { + return nextSize; + } final double previousSize = pixelSnapSizes[indexOfNextSize - 1]; if (initialVelocity.abs() <= tolerance.velocity) { // If velocity is zero, snap to the nearest snap size with the minimum velocity. diff --git a/lib/common/widgets/flutter/draggable_sheet/draggable_scrollable_sheet_topic.dart b/lib/common/widgets/flutter/draggable_sheet/draggable_scrollable_sheet_topic.dart index b9c1c8707..03e6c14c4 100644 --- a/lib/common/widgets/flutter/draggable_sheet/draggable_scrollable_sheet_topic.dart +++ b/lib/common/widgets/flutter/draggable_sheet/draggable_scrollable_sheet_topic.dart @@ -20,7 +20,7 @@ import 'dart:math' as math; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart' hide DraggableScrollableSheet; /// Controls a [DraggableScrollableSheet]. /// @@ -112,11 +112,10 @@ class DraggableScrollableController extends ChangeNotifier { _assertAttached(); assert(size >= 0 && size <= 1); assert(duration != Duration.zero); - final AnimationController animationController = - AnimationController.unbounded( - vsync: _attachedController!.position.context.vsync, - value: _attachedController!.extent.currentSize, - ); + final animationController = AnimationController.unbounded( + vsync: _attachedController!.position.context.vsync, + value: _attachedController!.extent.currentSize, + ); _animationControllers.add(animationController); _attachedController!.position.goIdle(); // This disables any snapping until the next user interaction with the sheet. @@ -587,7 +586,7 @@ class _DraggableScrollableSheetState extends State { } List _impliedSnapSizes() { - for (int index = 0; index < (widget.snapSizes?.length ?? 0); index += 1) { + for (var index = 0; index < (widget.snapSizes?.length ?? 0); index += 1) { final double snapSize = widget.snapSizes![index]; assert( snapSize >= widget.minChildSize && snapSize <= widget.maxChildSize, @@ -688,11 +687,11 @@ class _DraggableScrollableSheetState extends State { // have changed when the widget was updated. WidgetsBinding.instance.addPostFrameCallback((Duration timeStamp) { for ( - int index = 0; + var index = 0; index < _scrollController.positions.length; index++ ) { - final _DraggableScrollableSheetScrollPosition position = + final position = _scrollController.positions.elementAt(index) as _DraggableScrollableSheetScrollPosition; position.goBallistic(0); @@ -706,7 +705,7 @@ class _DraggableScrollableSheetState extends State { .asMap() .keys .map((int index) { - final String snapSizeString = widget.snapSizes![index].toString(); + final snapSizeString = widget.snapSizes![index].toString(); if (index == invalidIndex) { return '>>> $snapSizeString <<<'; } @@ -920,14 +919,10 @@ class _DraggableScrollableSheetScrollPosition ); } - final AnimationController ballisticController = - AnimationController.unbounded( - debugLabel: objectRuntimeType( - this, - '_DraggableScrollableSheetPosition', - ), - vsync: context.vsync, - ); + final ballisticController = AnimationController.unbounded( + debugLabel: objectRuntimeType(this, '_DraggableScrollableSheetPosition'), + vsync: context.vsync, + ); _ballisticControllers.add(ballisticController); double lastPosition = extent.currentPixels; @@ -1082,8 +1077,7 @@ class _InheritedResetNotifier extends InheritedNotifier<_ResetNotifier> { return false; } assert(widget is _InheritedResetNotifier); - final _InheritedResetNotifier inheritedNotifier = - widget as _InheritedResetNotifier; + final inheritedNotifier = widget as _InheritedResetNotifier; final bool wasCalled = inheritedNotifier.notifier!._wasCalled; inheritedNotifier.notifier!._wasCalled = false; return wasCalled; @@ -1160,6 +1154,10 @@ class _SnappingSimulation extends Simulation { return pixelSnapSizes.first; } final double nextSize = pixelSnapSizes[indexOfNextSize]; + // If already snapped - keep this as target size + if (nextSize == position) { + return nextSize; + } final double previousSize = pixelSnapSizes[indexOfNextSize - 1]; if (initialVelocity.abs() <= tolerance.velocity) { // If velocity is zero, snap to the nearest snap size with the minimum velocity. diff --git a/lib/common/widgets/flutter/dyn/button.dart b/lib/common/widgets/flutter/dyn/button.dart index 1b419b87d..0651f6813 100644 --- a/lib/common/widgets/flutter/dyn/button.dart +++ b/lib/common/widgets/flutter/dyn/button.dart @@ -15,7 +15,7 @@ import 'dart:math' as math; import 'package:PiliPlus/common/widgets/flutter/dyn/ink_well.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart' hide InkWell; +import 'package:flutter/material.dart' hide ButtonStyleButton, InkWell; import 'package:flutter/rendering.dart'; /// The base [StatefulWidget] class for buttons whose style is defined by a [ButtonStyle] object. @@ -121,7 +121,7 @@ abstract class ButtonStyleButton extends StatefulWidget { /// Defaults to true. final bool? isSemanticButton; - /// {@macro flutter.material.ButtonStyleButton.iconAlignment} + /// {@macro flutter.material.ButtonStyle.iconAlignment} @Deprecated( 'Remove this parameter as it is now ignored. ' 'Use ButtonStyle.iconAlignment instead. ' @@ -722,9 +722,9 @@ class _RenderInputPadding extends RenderShiftedBox { }) { if (child != null) { final Size childSize = layoutChild(child!, constraints); - final double height = math.max(childSize.width, minSize.width); - final double width = math.max(childSize.height, minSize.height); - return constraints.constrain(Size(height, width)); + final double width = math.max(childSize.width, minSize.width); + final double height = math.max(childSize.height, minSize.height); + return constraints.constrain(Size(width, height)); } return Size.zero; } @@ -764,7 +764,7 @@ class _RenderInputPadding extends RenderShiftedBox { layoutChild: ChildLayoutHelper.layoutChild, ); if (child != null) { - final BoxParentData childParentData = child!.parentData! as BoxParentData; + final childParentData = child!.parentData! as BoxParentData; childParentData.offset = Alignment.center.alongOffset( size - child!.size as Offset, ); diff --git a/lib/common/widgets/flutter/dyn/ink_well.dart b/lib/common/widgets/flutter/dyn/ink_well.dart index 9b9b3a445..5c456fc04 100644 --- a/lib/common/widgets/flutter/dyn/ink_well.dart +++ b/lib/common/widgets/flutter/dyn/ink_well.dart @@ -18,7 +18,7 @@ import 'dart:collection'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart' hide InkWell; import 'package:flutter/rendering.dart'; abstract class _ParentInkResponseState { @@ -265,14 +265,16 @@ class InkResponse extends StatelessWidget { /// The cursor for a mouse pointer when it enters or is hovering over the /// widget. /// + /// {@template flutter.material.InkWell.mouseCursor} /// If [mouseCursor] is a [WidgetStateMouseCursor], /// [WidgetStateProperty.resolve] is used for the following [WidgetState]s: /// /// * [WidgetState.hovered]. /// * [WidgetState.focused]. /// * [WidgetState.disabled]. + /// {@endtemplate} /// - /// If this property is null, [WidgetStateMouseCursor.clickable] will be used. + /// If this property is null, [WidgetStateMouseCursor.adaptiveClickable] will be used. final MouseCursor? mouseCursor; /// Whether this ink response should be clipped its bounds. @@ -638,7 +640,7 @@ class _InkResponseStateWidget extends StatefulWidget { @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); - final List gestures = [ + final gestures = [ if (onTap != null) 'tap', if (onDoubleTap != null) 'double tap', if (onLongPress != null) 'long press', @@ -900,7 +902,7 @@ class _InkResponseState extends State<_InkResponseStateWidget> _HighlightType.hover => widget.hoverColor ?? Theme.of(context).hoverColor, }; - final RenderBox referenceBox = context.findRenderObject()! as RenderBox; + final referenceBox = context.findRenderObject()! as RenderBox; _highlights[type] = InkHighlight( controller: Material.of(context), referenceBox: referenceBox, @@ -951,7 +953,7 @@ class _InkResponseState extends State<_InkResponseStateWidget> InteractiveInkFeature _createSplash(Offset globalPosition) { final MaterialInkController inkController = Material.of(context); - final RenderBox referenceBox = context.findRenderObject()! as RenderBox; + final referenceBox = context.findRenderObject()! as RenderBox; final Offset position = referenceBox.globalToLocal(globalPosition); final Color color = widget.overlayColor?.resolve(statesController.value) ?? @@ -1054,7 +1056,7 @@ class _InkResponseState extends State<_InkResponseStateWidget> final Offset globalPosition; if (context != null) { - final RenderBox referenceBox = context.findRenderObject()! as RenderBox; + final referenceBox = context.findRenderObject()! as RenderBox; assert( referenceBox.hasSize, 'InkResponse must be done with layout before starting a splash.', @@ -1139,7 +1141,7 @@ class _InkResponseState extends State<_InkResponseStateWidget> if (_splashes != null) { final Set splashes = _splashes!; _splashes = null; - for (final InteractiveInkFeature splash in splashes) { + for (final splash in splashes) { splash.dispose(); } _currentSplash = null; @@ -1205,7 +1207,7 @@ class _InkResponseState extends State<_InkResponseStateWidget> assert(widget.debugCheckContext(context)); final ThemeData theme = Theme.of(context); - const Set highlightableStates = { + const highlightableStates = { WidgetState.focused, WidgetState.hovered, WidgetState.pressed, @@ -1216,15 +1218,15 @@ class _InkResponseState extends State<_InkResponseStateWidget> ); // Each highlightable state will be resolved separately to get the corresponding color. // For this resolution to be correct, the non-highlightable states should be preserved. - final Set pressed = { + final pressed = { ...nonHighlightableStates, WidgetState.pressed, }; - final Set focused = { + final focused = { ...nonHighlightableStates, WidgetState.focused, }; - final Set hovered = { + final hovered = { ...nonHighlightableStates, WidgetState.hovered, }; @@ -1260,7 +1262,7 @@ class _InkResponseState extends State<_InkResponseStateWidget> final MouseCursor effectiveMouseCursor = WidgetStateProperty.resolveAs( - widget.mouseCursor ?? WidgetStateMouseCursor.clickable, + widget.mouseCursor ?? WidgetStateMouseCursor.adaptiveClickable, statesController.value, ); diff --git a/lib/common/widgets/flutter/dyn/text_button.dart b/lib/common/widgets/flutter/dyn/text_button.dart index 948f20e20..3dce452c4 100644 --- a/lib/common/widgets/flutter/dyn/text_button.dart +++ b/lib/common/widgets/flutter/dyn/text_button.dart @@ -14,7 +14,8 @@ import 'dart:ui' show lerpDouble; import 'package:PiliPlus/common/widgets/flutter/dyn/button.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart' hide InkWell, ButtonStyleButton; +import 'package:flutter/material.dart' + hide ButtonStyleButton, TextButton, InkWell; /// A Material Design "Text Button". /// @@ -84,7 +85,7 @@ class TextButton extends ButtonStyleButton { super.statesController, super.isSemanticButton, required Widget super.child, - }); + }) : _addPadding = false; /// Create a text button from a pair of widgets that serve as the button's /// [icon] and [label]. @@ -92,56 +93,38 @@ class TextButton extends ButtonStyleButton { /// The icon and label are arranged in a row and padded by 8 logical pixels /// at the ends, with an 8 pixel gap in between. /// - /// If [icon] is null, will create a [TextButton] instead. + /// If [icon] is null, this constructor will create a [TextButton] + /// that doesn't display an icon. /// - /// {@macro flutter.material.ButtonStyleButton.iconAlignment} + /// {@macro flutter.material.ButtonStyle.iconAlignment} /// - factory TextButton.icon({ - Key? key, - required VoidCallback? onPressed, - VoidCallback? onLongPress, - ValueChanged? onHover, - ValueChanged? onFocusChange, - ButtonStyle? style, - FocusNode? focusNode, - bool? autofocus, - Clip? clipBehavior, - WidgetStatesController? statesController, + TextButton.icon({ + super.key, + required super.onPressed, + super.onLongPress, + super.onHover, + super.onFocusChange, + super.style, + super.focusNode, + super.autofocus = false, + super.clipBehavior = Clip.none, + super.statesController, Widget? icon, required Widget label, IconAlignment? iconAlignment, - }) { - if (icon == null) { - return TextButton( - key: key, - onPressed: onPressed, - onLongPress: onLongPress, - onHover: onHover, - onFocusChange: onFocusChange, - style: style, - focusNode: focusNode, - autofocus: autofocus ?? false, - clipBehavior: clipBehavior ?? Clip.none, - statesController: statesController, - child: label, - ); - } - return _TextButtonWithIcon( - key: key, - onPressed: onPressed, - onLongPress: onLongPress, - onHover: onHover, - onFocusChange: onFocusChange, - style: style, - focusNode: focusNode, - autofocus: autofocus ?? false, - clipBehavior: clipBehavior ?? Clip.none, - statesController: statesController, - icon: icon, - label: label, - iconAlignment: iconAlignment, - ); - } + }) : _addPadding = icon != null, + super( + child: icon != null + ? _TextButtonWithIconChild( + label: label, + icon: icon, + buttonStyle: style, + iconAlignment: iconAlignment, + ) + : label, + ); + + final bool _addPadding; /// A static convenience method that constructs a text button /// [ButtonStyle] given simple values. @@ -339,9 +322,7 @@ class TextButton extends ButtonStyleButton { /// * `maximumSize` - Size.infinite /// * `side` - null /// * `shape` - RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)) - /// * `mouseCursor` - /// * disabled - SystemMouseCursors.basic - /// * others - SystemMouseCursors.click + /// * `mouseCursor` - WidgetStateMouseCursor.adaptiveClickable /// * `visualDensity` - theme.visualDensity /// * `tapTargetSize` - theme.materialTapTargetSize /// * `animationDuration` - kThemeChangeDuration @@ -389,9 +370,7 @@ class TextButton extends ButtonStyleButton { /// * `maximumSize` - Size.infinite /// * `side` - null /// * `shape` - StadiumBorder() - /// * `mouseCursor` - /// * disabled - SystemMouseCursors.basic - /// * others - SystemMouseCursors.click + /// * `mouseCursor` - WidgetStateMouseCursor.adaptiveClickable /// * `visualDensity` - theme.visualDensity /// * `tapTargetSize` - theme.materialTapTargetSize /// * `animationDuration` - kThemeChangeDuration @@ -406,8 +385,7 @@ class TextButton extends ButtonStyleButton { ButtonStyle defaultStyleOf(BuildContext context) { final ThemeData theme = Theme.of(context); final ColorScheme colorScheme = theme.colorScheme; - - return Theme.of(context).useMaterial3 + final ButtonStyle buttonStyle = theme.useMaterial3 ? _TextButtonDefaultsM3(context) : styleFrom( foregroundColor: colorScheme.primary, @@ -425,7 +403,9 @@ class TextButton extends ButtonStyleButton { shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(4)), ), - enabledMouseCursor: SystemMouseCursors.click, + enabledMouseCursor: kIsWeb + ? SystemMouseCursors.click + : SystemMouseCursors.basic, disabledMouseCursor: SystemMouseCursors.basic, visualDensity: theme.visualDensity, tapTargetSize: theme.materialTapTargetSize, @@ -434,6 +414,28 @@ class TextButton extends ButtonStyleButton { alignment: Alignment.center, splashFactory: InkRipple.splashFactory, ); + + // Only apply padding when TextButton has an Icon. + if (_addPadding) { + final double defaultFontSize = + buttonStyle.textStyle?.resolve(const {})?.fontSize ?? + 14.0; + final double effectiveTextScale = + MediaQuery.textScalerOf(context).scale(defaultFontSize) / 14.0; + final EdgeInsetsGeometry scaledPadding = ButtonStyleButton.scaledPadding( + theme.useMaterial3 + ? const EdgeInsetsDirectional.fromSTEB(12, 8, 16, 8) + : const EdgeInsets.all(8), + const EdgeInsets.symmetric(horizontal: 4), + const EdgeInsets.symmetric(horizontal: 4), + effectiveTextScale, + ); + return buttonStyle.copyWith( + padding: WidgetStatePropertyAll(scaledPadding), + ); + } + + return buttonStyle; } /// Returns the [TextButtonThemeData.style] of the closest @@ -459,53 +461,6 @@ EdgeInsetsGeometry _scaledPadding(BuildContext context) { ); } -class _TextButtonWithIcon extends TextButton { - _TextButtonWithIcon({ - super.key, - required super.onPressed, - super.onLongPress, - super.onHover, - super.onFocusChange, - super.style, - super.focusNode, - bool? autofocus, - super.clipBehavior, - super.statesController, - required Widget icon, - required Widget label, - IconAlignment? iconAlignment, - }) : super( - autofocus: autofocus ?? false, - child: _TextButtonWithIconChild( - icon: icon, - label: label, - buttonStyle: style, - iconAlignment: iconAlignment, - ), - ); - - @override - ButtonStyle defaultStyleOf(BuildContext context) { - final bool useMaterial3 = Theme.of(context).useMaterial3; - final ButtonStyle buttonStyle = super.defaultStyleOf(context); - final double defaultFontSize = - buttonStyle.textStyle?.resolve(const {})?.fontSize ?? 14.0; - final double effectiveTextScale = - MediaQuery.textScalerOf(context).scale(defaultFontSize) / 14.0; - final EdgeInsetsGeometry scaledPadding = ButtonStyleButton.scaledPadding( - useMaterial3 - ? const EdgeInsetsDirectional.fromSTEB(12, 8, 16, 8) - : const EdgeInsets.all(8), - const EdgeInsets.symmetric(horizontal: 4), - const EdgeInsets.symmetric(horizontal: 4), - effectiveTextScale, - ); - return buttonStyle.copyWith( - padding: WidgetStatePropertyAll(scaledPadding), - ); - } -} - class _TextButtonWithIconChild extends StatelessWidget { const _TextButtonWithIconChild({ required this.label, @@ -557,72 +512,72 @@ class _TextButtonWithIconChild extends StatelessWidget { // dart format off class _TextButtonDefaultsM3 extends ButtonStyle { _TextButtonDefaultsM3(this.context) - : super( - animationDuration: kThemeChangeDuration, - enableFeedback: true, - alignment: Alignment.center, - ); + : super( + animationDuration: kThemeChangeDuration, + enableFeedback: true, + alignment: Alignment.center, + ); final BuildContext context; late final ColorScheme _colors = Theme.of(context).colorScheme; @override WidgetStateProperty get textStyle => - WidgetStatePropertyAll(Theme.of(context).textTheme.labelLarge); + WidgetStatePropertyAll(Theme.of(context).textTheme.labelLarge); @override WidgetStateProperty? get backgroundColor => - const WidgetStatePropertyAll(Colors.transparent); + const WidgetStatePropertyAll(Colors.transparent); @override WidgetStateProperty? get foregroundColor => - WidgetStateProperty.resolveWith((Set states) { - if (states.contains(WidgetState.disabled)) { - return _colors.onSurface.withValues(alpha: 0.38); - } - return _colors.primary; - }); + WidgetStateProperty.resolveWith((Set states) { + if (states.contains(WidgetState.disabled)) { + return _colors.onSurface.withValues(alpha: 0.38); + } + return _colors.primary; + }); @override WidgetStateProperty? get overlayColor => - WidgetStateProperty.resolveWith((Set states) { - if (states.contains(WidgetState.pressed)) { - return _colors.primary.withValues(alpha: 0.1); - } - if (states.contains(WidgetState.hovered)) { - return _colors.primary.withValues(alpha: 0.08); - } - if (states.contains(WidgetState.focused)) { - return _colors.primary.withValues(alpha: 0.1); - } - return null; - }); + WidgetStateProperty.resolveWith((Set states) { + if (states.contains(WidgetState.pressed)) { + return _colors.primary.withValues(alpha: 0.1); + } + if (states.contains(WidgetState.hovered)) { + return _colors.primary.withValues(alpha: 0.08); + } + if (states.contains(WidgetState.focused)) { + return _colors.primary.withValues(alpha: 0.1); + } + return null; + }); @override WidgetStateProperty? get shadowColor => - const WidgetStatePropertyAll(Colors.transparent); + const WidgetStatePropertyAll(Colors.transparent); @override WidgetStateProperty? get surfaceTintColor => - const WidgetStatePropertyAll(Colors.transparent); + const WidgetStatePropertyAll(Colors.transparent); @override WidgetStateProperty? get elevation => - const WidgetStatePropertyAll(0.0); + const WidgetStatePropertyAll(0.0); @override WidgetStateProperty? get padding => - WidgetStatePropertyAll(_scaledPadding(context)); + WidgetStatePropertyAll(_scaledPadding(context)); @override WidgetStateProperty? get minimumSize => - const WidgetStatePropertyAll(Size(64.0, 40.0)); + const WidgetStatePropertyAll(Size(64.0, 40.0)); // No default fixedSize @override WidgetStateProperty? get iconSize => - const WidgetStatePropertyAll(18.0); + const WidgetStatePropertyAll(18.0); @override WidgetStateProperty? get iconColor { @@ -645,22 +600,16 @@ class _TextButtonDefaultsM3 extends ButtonStyle { @override WidgetStateProperty? get maximumSize => - const WidgetStatePropertyAll(Size.infinite); + const WidgetStatePropertyAll(Size.infinite); // No default side @override WidgetStateProperty? get shape => - const WidgetStatePropertyAll(StadiumBorder()); + const WidgetStatePropertyAll(StadiumBorder()); @override - WidgetStateProperty? get mouseCursor => - WidgetStateProperty.resolveWith((Set states) { - if (states.contains(WidgetState.disabled)) { - return SystemMouseCursors.basic; - } - return SystemMouseCursors.click; - }); + WidgetStateProperty? get mouseCursor => WidgetStateMouseCursor.adaptiveClickable; @override VisualDensity? get visualDensity => Theme.of(context).visualDensity; diff --git a/lib/common/widgets/flutter/list_tile.dart b/lib/common/widgets/flutter/list_tile.dart index 50749e80f..048fe1400 100644 --- a/lib/common/widgets/flutter/list_tile.dart +++ b/lib/common/widgets/flutter/list_tile.dart @@ -20,7 +20,7 @@ library; import 'dart:math' as math; -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart' hide ListTile; import 'package:flutter/rendering.dart'; // Examples can assume: @@ -1506,11 +1506,16 @@ class _RenderListTile extends RenderBox @override double computeMinIntrinsicHeight(double width) { - return math.max( - _targetTileHeight, - title.getMinIntrinsicHeight(width) + - (subtitle?.getMinIntrinsicHeight(width) ?? 0.0), - ); + final double titleMinHeight = title.getMinIntrinsicHeight(width); + final double? subtitleMinHeight = subtitle?.getMinIntrinsicHeight(width); + + const topAndBottomPaddingMultiplier = 2; + final double contentHeight = + titleMinHeight + + (subtitleMinHeight ?? 0.0) + + topAndBottomPaddingMultiplier * _minVerticalPadding; + + return math.max(_targetTileHeight, contentHeight); } @override diff --git a/lib/common/widgets/flutter/page/page_view.dart b/lib/common/widgets/flutter/page/page_view.dart index 1cf5f0786..b61ef905d 100644 --- a/lib/common/widgets/flutter/page/page_view.dart +++ b/lib/common/widgets/flutter/page/page_view.dart @@ -2,18 +2,11 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// ignore_for_file: uri_does_not_exist_in_doc_import - -/// @docImport 'package:flutter/material.dart'; -/// -/// @docImport 'single_child_scroll_view.dart'; -/// @docImport 'text.dart'; -library; - import 'package:PiliPlus/common/widgets/flutter/page/scrollable.dart'; import 'package:flutter/gestures.dart' show DragStartBehavior, HorizontalDragGestureRecognizer; -import 'package:flutter/material.dart' hide Scrollable, ScrollableState; +import 'package:flutter/material.dart' + hide PageView, Scrollable, ScrollableState; import 'package:flutter/rendering.dart'; class _ForceImplicitScrollPhysics extends ScrollPhysics { @@ -378,7 +371,7 @@ class _PageViewState if (notification.depth == 0 && widget.onPageChanged != null && notification is ScrollUpdateNotification) { - final PageMetrics metrics = notification.metrics as PageMetrics; + final metrics = notification.metrics as PageMetrics; final int currentPage = metrics.page!.round(); if (currentPage != _lastReportedPage) { _lastReportedPage = currentPage; diff --git a/lib/common/widgets/flutter/page/scrollable.dart b/lib/common/widgets/flutter/page/scrollable.dart index 1730badfe..f1876e5f3 100644 --- a/lib/common/widgets/flutter/page/scrollable.dart +++ b/lib/common/widgets/flutter/page/scrollable.dart @@ -2,20 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// ignore_for_file: uri_does_not_exist_in_doc_import - -/// @docImport 'package:flutter/material.dart'; -/// -/// @docImport 'page_storage.dart'; -/// @docImport 'page_view.dart'; -/// @docImport 'scroll_metrics.dart'; -/// @docImport 'scroll_notification.dart'; -/// @docImport 'scroll_view.dart'; -/// @docImport 'single_child_scroll_view.dart'; -/// @docImport 'two_dimensional_scroll_view.dart'; -/// @docImport 'two_dimensional_viewport.dart'; -library; - import 'dart:async'; import 'dart:math' as math; @@ -350,7 +336,7 @@ class Scrollable /// if no [Scrollable] ancestor is found. static ScrollableState? maybeOf(BuildContext context, {Axis? axis}) { // This is the context that will need to establish the dependency. - final BuildContext originalContext = context; + final originalContext = context; InheritedElement? element = context .getElementForInheritedWidgetOfExactType<_ScrollableScope>(); while (element != null) { @@ -477,7 +463,7 @@ class Scrollable ScrollPositionAlignmentPolicy alignmentPolicy = ScrollPositionAlignmentPolicy.explicit, }) { - final List> futures = >[]; + final futures = >[]; // The targetRenderObject is used to record the first target renderObject. // If there are multiple scrollable widgets nested, the targetRenderObject @@ -855,7 +841,7 @@ class ScrollableState } _shouldIgnorePointer = value; if (_ignorePointerKey.currentContext != null) { - final RenderIgnorePointer renderBox = + final renderBox = _ignorePointerKey.currentContext!.findRenderObject()! as RenderIgnorePointer; renderBox.ignoring = _shouldIgnorePointer; @@ -1014,7 +1000,7 @@ class ScrollableState } Widget _buildChrome(BuildContext context, Widget child) { - final ScrollableDetails details = ScrollableDetails( + final details = ScrollableDetails( direction: widget.axisDirection, controller: _effectiveScrollController, decorationClipBehavior: widget.clipBehavior, @@ -1344,7 +1330,7 @@ class _ScrollableSelectionContainerDelegate } Offset _inferPositionRelatedToOrigin(Offset globalPosition) { - final RenderBox box = state.context.findRenderObject()! as RenderBox; + final box = state.context.findRenderObject()! as RenderBox; final Offset localPosition = box.globalToLocal(globalPosition); if (!_selectionStartsInScrollable) { // If the selection starts outside of the scrollable, selecting across the @@ -1377,7 +1363,7 @@ class _ScrollableSelectionContainerDelegate bool forceUpdateEnd = true, }) { final Offset deltaToOrigin = _getDeltaToScrollOrigin(state); - final RenderBox box = state.context.findRenderObject()! as RenderBox; + final box = state.context.findRenderObject()! as RenderBox; final Matrix4 transform = box.getTransformTo(null); if (currentSelectionStartIndex != -1 && (_currentDragStartRelatedToOrigin == null || forceUpdateStart)) { @@ -1492,14 +1478,13 @@ class _ScrollableSelectionContainerDelegate if (lineHeight == null || edge == null) { return; } - final RenderBox scrollableBox = - state.context.findRenderObject()! as RenderBox; + final scrollableBox = state.context.findRenderObject()! as RenderBox; final Matrix4 transform = selectable.getTransformTo(scrollableBox); final Offset edgeOffsetInScrollableCoordinates = MatrixUtils.transformPoint( transform, edge.localPosition, ); - final Rect scrollableRect = Rect.fromLTRB( + final scrollableRect = Rect.fromLTRB( 0, 0, scrollableBox.size.width, @@ -1568,9 +1553,9 @@ class _ScrollableSelectionContainerDelegate } bool _globalPositionInScrollable(Offset globalPosition) { - final RenderBox box = state.context.findRenderObject()! as RenderBox; + final box = state.context.findRenderObject()! as RenderBox; final Offset localPosition = box.globalToLocal(globalPosition); - final Rect rect = Rect.fromLTRB(0, 0, box.size.width, box.size.height); + final rect = Rect.fromLTRB(0, 0, box.size.width, box.size.height); return rect.contains(localPosition); } @@ -1818,9 +1803,9 @@ class _RenderScrollSemantics extends RenderProxyBox { (_innerNode ??= SemanticsNode(showOnScreen: showOnScreen)).rect = node.rect; int? firstVisibleIndex; - final List excluded = [_innerNode!]; - final List included = []; - for (final SemanticsNode child in children) { + final excluded = [_innerNode!]; + final included = []; + for (final child in children) { assert(child.isTagged(RenderViewport.useTwoPaneSemantics)); if (child.isTagged(RenderViewport.excludeFromScrolling)) { excluded.add(child); diff --git a/lib/common/widgets/flutter/page/scrollable_helpers.dart b/lib/common/widgets/flutter/page/scrollable_helpers.dart index 895de8852..53a2b2bb7 100644 --- a/lib/common/widgets/flutter/page/scrollable_helpers.dart +++ b/lib/common/widgets/flutter/page/scrollable_helpers.dart @@ -13,7 +13,8 @@ import 'dart:math' as math; import 'package:PiliPlus/common/widgets/flutter/page/scrollable.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart' hide ScrollableState; +import 'package:flutter/material.dart' + hide EdgeDraggingAutoScroller, Scrollable, ScrollableState; /// An auto scroller that scrolls the [scrollable] if a drag gesture drags close /// to its edge. @@ -29,7 +30,7 @@ class EdgeDraggingAutoScroller { required this.velocityScalar, }); - /// The [CustomScrollable] this auto scroller is scrolling. + /// The [Scrollable] this auto scroller is scrolling. final ScrollableState scrollable; /// Called when a scroll view is scrolled. @@ -97,8 +98,7 @@ class EdgeDraggingAutoScroller { } Future _scroll() async { - final RenderBox scrollRenderBox = - scrollable.context.findRenderObject()! as RenderBox; + final scrollRenderBox = scrollable.context.findRenderObject()! as RenderBox; final Matrix4 transform = scrollRenderBox.getTransformTo(null); final Rect globalRect = MatrixUtils.transformRect( transform, @@ -123,7 +123,7 @@ class EdgeDraggingAutoScroller { ); _scrolling = true; double? newOffset; - const double overDragMax = 20.0; + const overDragMax = 20.0; final Offset deltaToOrigin = scrollable.deltaToScrollOrigin; final Offset viewportOrigin = globalRect.topLeft.translate( @@ -194,9 +194,7 @@ class EdgeDraggingAutoScroller { _scrolling = false; return; } - final Duration duration = Duration( - milliseconds: (1000 / velocityScalar).round(), - ); + final duration = Duration(milliseconds: (1000 / velocityScalar).round()); await scrollable.position.animateTo( newOffset, duration: duration, diff --git a/lib/common/widgets/flutter/page/tabs.dart b/lib/common/widgets/flutter/page/tabs.dart index 1e5c4579e..24be4d084 100644 --- a/lib/common/widgets/flutter/page/tabs.dart +++ b/lib/common/widgets/flutter/page/tabs.dart @@ -6,7 +6,7 @@ import 'package:PiliPlus/common/widgets/flutter/page/page_view.dart'; import 'package:flutter/foundation.dart' show clampDouble; import 'package:flutter/gestures.dart' show DragStartBehavior, HorizontalDragGestureRecognizer; -import 'package:flutter/material.dart' hide PageView; +import 'package:flutter/material.dart' hide TabBarView, PageView; /// A page view that displays the widget which corresponds to the currently /// selected tab. @@ -220,7 +220,7 @@ class _TabBarViewState return; } - final bool adjacentDestination = + final adjacentDestination = (_currentIndex! - _controller!.previousIndex).abs() == 1; if (adjacentDestination) { _warpToAdjacentTab(_controller!.animationDuration); diff --git a/lib/common/widgets/flutter/refresh_indicator.dart b/lib/common/widgets/flutter/refresh_indicator.dart index db7fcd516..a24b045ed 100644 --- a/lib/common/widgets/flutter/refresh_indicator.dart +++ b/lib/common/widgets/flutter/refresh_indicator.dart @@ -149,7 +149,6 @@ class RefreshIndicator extends StatefulWidget { /// The [semanticsValue] may be used to specify progress on the widget. const RefreshIndicator({ super.key, - required this.child, this.displacement = 40.0, this.edgeOffset = 0.0, required this.onRefresh, @@ -161,6 +160,7 @@ class RefreshIndicator extends StatefulWidget { this.strokeWidth = RefreshProgressIndicator.defaultStrokeWidth, this.triggerMode = RefreshIndicatorTriggerMode.onEdge, this.elevation = 2.0, + required this.child, }) : _indicatorType = _IndicatorType.material, onStatusChange = null, assert(elevation >= 0.0); @@ -183,7 +183,6 @@ class RefreshIndicator extends StatefulWidget { /// from [CupertinoSliverRefreshControl], due to a difference in structure. const RefreshIndicator.adaptive({ super.key, - required this.child, this.displacement = 40.0, this.edgeOffset = 0.0, required this.onRefresh, @@ -195,6 +194,7 @@ class RefreshIndicator extends StatefulWidget { this.strokeWidth = RefreshProgressIndicator.defaultStrokeWidth, this.triggerMode = RefreshIndicatorTriggerMode.onEdge, this.elevation = 2.0, + required this.child, }) : _indicatorType = _IndicatorType.adaptive, onStatusChange = null, assert(elevation >= 0.0); @@ -205,7 +205,6 @@ class RefreshIndicator extends StatefulWidget { /// Events can be optionally listened by using the `onStatusChange` callback. const RefreshIndicator.noSpinner({ super.key, - required this.child, required this.onRefresh, this.onStatusChange, this.notificationPredicate = defaultScrollNotificationPredicate, @@ -213,6 +212,7 @@ class RefreshIndicator extends StatefulWidget { this.semanticsValue, this.triggerMode = RefreshIndicatorTriggerMode.onEdge, this.elevation = 2.0, + required this.child, }) : _indicatorType = _IndicatorType.noSpinner, // The following parameters aren't used because [_IndicatorType.noSpinner] is being used, // which involves showing no spinner, hence the following parameters are useless since diff --git a/lib/common/widgets/flutter/selectable_text/selectable_region.dart b/lib/common/widgets/flutter/selectable_text/selectable_region.dart index 9598cab6a..1d9feac6a 100644 --- a/lib/common/widgets/flutter/selectable_text/selectable_region.dart +++ b/lib/common/widgets/flutter/selectable_text/selectable_region.dart @@ -12,7 +12,7 @@ import 'package:flutter/gestures.dart' BaseTapAndDragGestureRecognizer, TapAndHorizontalDragGestureRecognizer, TapAndPanGestureRecognizer; -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart' hide SelectableRegion; import 'package:flutter/rendering.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; @@ -277,26 +277,28 @@ class SelectableRegion extends StatefulWidget { required final VoidCallback onSelectAll, required final VoidCallback? onShare, }) { - final bool canCopy = - selectionGeometry.status == SelectionStatus.uncollapsed; + final canCopy = selectionGeometry.status == SelectionStatus.uncollapsed; final bool canSelectAll = selectionGeometry.hasContent; - final bool platformCanShare = switch (defaultTargetPlatform) { - TargetPlatform.android => - selectionGeometry.status == SelectionStatus.uncollapsed, - TargetPlatform.macOS || - TargetPlatform.fuchsia || - TargetPlatform.linux || - TargetPlatform.windows => false, - // TODO(bleroux): the share button should be shown on iOS but the share - // functionality requires some changes on the engine side because, on iPad, - // it needs an anchor for the popup. - // See: https://github.com/flutter/flutter/issues/141775. - TargetPlatform.iOS => false, - }; + // The share button is not supported on the web. + final bool platformCanShare = + !kIsWeb && + switch (defaultTargetPlatform) { + TargetPlatform.android => + selectionGeometry.status == SelectionStatus.uncollapsed, + TargetPlatform.macOS || + TargetPlatform.fuchsia || + TargetPlatform.linux || + TargetPlatform.windows => false, + // TODO(bleroux): the share button should be shown on iOS but the share + // functionality requires some changes on the engine side because, on iPad, + // it needs an anchor for the popup. + // See: https://github.com/flutter/flutter/issues/141775. + TargetPlatform.iOS => false, + }; final bool canShare = onShare != null && platformCanShare; // On Android, the share button is before the select all button. - final bool showShareBeforeSelectAll = + final showShareBeforeSelectAll = defaultTargetPlatform == TargetPlatform.android; // Determine which buttons will appear so that the order and total number is @@ -410,6 +412,16 @@ class SelectableRegionState extends State Orientation? _lastOrientation; SelectedContent? _lastSelectedContent; + /// Whether the native browser context menu is enabled. + // TODO(Renzo-Olivares): Re-enable web context menu for Android + // and iOS when https://github.com/flutter/flutter/issues/177123 + // is resolved. + bool get _webContextMenuEnabled => + kIsWeb && + BrowserContextMenu.enabled && + defaultTargetPlatform != TargetPlatform.android && + defaultTargetPlatform != TargetPlatform.iOS; + /// The [SelectionOverlay] that is currently visible on the screen. /// /// Can be null if there is no visible [SelectionOverlay]. @@ -513,7 +525,7 @@ class SelectableRegionState extends State void _handleFocusChanged() { if (!_focusNode.hasFocus) { - if (kIsWeb) { + if (_webContextMenuEnabled) { PlatformSelectableRegionContextMenu.detach(_selectionDelegate); } if (SchedulerBinding.instance.lifecycleState == @@ -531,7 +543,7 @@ class SelectableRegionState extends State _finalizeSelectableRegionStatus(); } } - if (kIsWeb) { + if (_webContextMenuEnabled) { PlatformSelectableRegionContextMenu.attach(_selectionDelegate); } } @@ -596,7 +608,7 @@ class SelectableRegionState extends State // This method should be used in all instances when details.consecutiveTapCount // would be used. int _getEffectiveConsecutiveTapCount(int rawCount) { - int maxConsecutiveTap = 3; + var maxConsecutiveTap = 3; switch (defaultTargetPlatform) { case TargetPlatform.android: case TargetPlatform.fuchsia: @@ -1177,21 +1189,9 @@ class SelectableRegionState extends State _selectionOverlay != null && (_selectionOverlay!.isDraggingStartHandle || _selectionOverlay!.isDraggingEndHandle); - if (widget.selectionControls is! TextSelectionHandleControls) { - if (!draggingHandles) { - _selectionOverlay!.hideMagnifier(); - _selectionOverlay!.showToolbar(); - } - } else { - if (!draggingHandles) { - _selectionOverlay!.hideMagnifier(); - _selectionOverlay!.showToolbar( - context: context, - contextMenuBuilder: (BuildContext context) { - return widget.contextMenuBuilder!(context, this); - }, - ); - } + if (!draggingHandles) { + _selectionOverlay!.hideMagnifier(); + _showToolbar(); } _finalizeSelection(); _updateSelectedContentIfNeeded(); @@ -1335,13 +1335,13 @@ class SelectableRegionState extends State final Vector3 globalTransform = _selectable! .getTransformTo(null) .getTranslation(); - final Offset globalTransformAsOffset = Offset( + final globalTransformAsOffset = Offset( globalTransform.x, globalTransform.y, ); final Offset globalSelectionPointPosition = selectionPoint.localPosition + globalTransformAsOffset; - final Rect caretRect = Rect.fromLTWH( + final caretRect = Rect.fromLTWH( globalSelectionPointPosition.dx, globalSelectionPointPosition.dy - selectionPoint.lineHeight, 0, @@ -1439,7 +1439,7 @@ class SelectableRegionState extends State // functionality depending on the browser (such as translate). Due to this, // we should not show a Flutter toolbar for the editable text elements // unless the browser's context menu is explicitly disabled. - if (kIsWeb && BrowserContextMenu.enabled) { + if (_webContextMenuEnabled) { return false; } @@ -1448,6 +1448,9 @@ class SelectableRegionState extends State } _selectionOverlay!.toolbarLocation = location; + // TODO(Renzo-Olivares): Remove the logic below that does a runtimeType + // check for TextSelectionHandleControls when TextSelectionHandleControls + // is fully removed, see: https://github.com/flutter/flutter/pull/124262. if (widget.selectionControls is! TextSelectionHandleControls) { _selectionOverlay!.showToolbar(); return true; @@ -1683,7 +1686,7 @@ class SelectableRegionState extends State /// for the default context menu buttons. TextSelectionToolbarAnchors get contextMenuAnchors { if (_lastSecondaryTapDownPosition != null) { - final TextSelectionToolbarAnchors anchors = TextSelectionToolbarAnchors( + final anchors = TextSelectionToolbarAnchors( primaryAnchor: _lastSecondaryTapDownPosition!, ); // Clear the state of _lastSecondaryTapDownPosition after use since a user may @@ -1691,7 +1694,7 @@ class SelectableRegionState extends State _lastSecondaryTapDownPosition = null; return anchors; } - final RenderBox renderBox = context.findRenderObject()! as RenderBox; + final renderBox = context.findRenderObject()! as RenderBox; return TextSelectionToolbarAnchors.fromSelection( renderBox: renderBox, startGlyphHeight: startGlyphHeight, @@ -1843,7 +1846,7 @@ class SelectableRegionState extends State } List get _textProcessingActionButtonItems { - final List buttonItems = []; + final buttonItems = []; final SelectedContent? data = _selectable?.getSelectedContent(); if (data == null) { return buttonItems; @@ -2048,7 +2051,7 @@ class SelectableRegionState extends State child: widget.child, ), ); - if (kIsWeb) { + if (_webContextMenuEnabled) { result = PlatformSelectableRegionContextMenu(child: result); } return CompositedTransformTarget( diff --git a/lib/common/widgets/flutter/selectable_text/selectable_text.dart b/lib/common/widgets/flutter/selectable_text/selectable_text.dart index c73e49f34..0ef7989ee 100644 --- a/lib/common/widgets/flutter/selectable_text/selectable_text.dart +++ b/lib/common/widgets/flutter/selectable_text/selectable_text.dart @@ -8,7 +8,8 @@ import 'package:PiliPlus/common/widgets/flutter/selectable_text/text_selection.d import 'package:flutter/cupertino.dart' hide TextSelectionGestureDetectorBuilder; import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart' hide TextSelectionGestureDetectorBuilder; +import 'package:flutter/material.dart' + hide SelectableText, TextSelectionGestureDetectorBuilder; import 'package:flutter/rendering.dart'; import 'package:flutter/scheduler.dart'; @@ -610,8 +611,9 @@ class _SelectableTextState extends State super.didUpdateWidget(oldWidget); if (widget.data != oldWidget.data || widget.textSpan != oldWidget.textSpan) { - _controller.removeListener(_onControllerChanged); - _controller.dispose(); + _controller + ..removeListener(_onControllerChanged) + ..dispose(); _controller = _TextSpanEditingController( textSpan: widget.textSpan ?? TextSpan(text: widget.data), ); @@ -766,7 +768,7 @@ class _SelectableTextState extends State cupertinoTheme.primaryColor; selectionColor = selectionStyle.selectionColor ?? - cupertinoTheme.primaryColor.withOpacity(0.40); + cupertinoTheme.primaryColor.withValues(alpha: 0.40); cursorRadius ??= const Radius.circular(2.0); cursorOffset = Offset( iOSHorizontalOffset / MediaQuery.devicePixelRatioOf(context), @@ -785,7 +787,7 @@ class _SelectableTextState extends State cupertinoTheme.primaryColor; selectionColor = selectionStyle.selectionColor ?? - cupertinoTheme.primaryColor.withOpacity(0.40); + cupertinoTheme.primaryColor.withValues(alpha: 0.40); cursorRadius ??= const Radius.circular(2.0); cursorOffset = Offset( iOSHorizontalOffset / MediaQuery.devicePixelRatioOf(context), @@ -804,7 +806,7 @@ class _SelectableTextState extends State theme.colorScheme.primary; selectionColor = selectionStyle.selectionColor ?? - theme.colorScheme.primary.withOpacity(0.40); + theme.colorScheme.primary.withValues(alpha: 0.40); case TargetPlatform.linux: case TargetPlatform.windows: @@ -818,7 +820,7 @@ class _SelectableTextState extends State theme.colorScheme.primary; selectionColor = selectionStyle.selectionColor ?? - theme.colorScheme.primary.withOpacity(0.40); + theme.colorScheme.primary.withValues(alpha: 0.40); } final DefaultTextStyle defaultTextStyle = DefaultTextStyle.of(context); diff --git a/lib/common/widgets/flutter/selectable_text/selection_area.dart b/lib/common/widgets/flutter/selectable_text/selection_area.dart index 6e4c0c6ba..d001462c8 100644 --- a/lib/common/widgets/flutter/selectable_text/selection_area.dart +++ b/lib/common/widgets/flutter/selectable_text/selection_area.dart @@ -10,6 +10,7 @@ import 'package:flutter/cupertino.dart' SelectableRegionContextMenuBuilder; import 'package:flutter/material.dart' hide + SelectionArea, SelectableRegion, SelectableRegionState, SelectableRegionContextMenuBuilder; diff --git a/lib/common/widgets/flutter/selectable_text/tap_and_drag.dart b/lib/common/widgets/flutter/selectable_text/tap_and_drag.dart index 6712f94dd..7884f7469 100644 --- a/lib/common/widgets/flutter/selectable_text/tap_and_drag.dart +++ b/lib/common/widgets/flutter/selectable_text/tap_and_drag.dart @@ -5,7 +5,8 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart'; +import 'package:flutter/gestures.dart' + hide TapAndHorizontalDragGestureRecognizer; // Examples can assume: // void setState(VoidCallback fn) { } @@ -823,7 +824,7 @@ sealed class BaseTapAndDragGestureRecognizer untransformedDelta: localDelta, untransformedEndPosition: correctedLocalPosition, ); - final OffsetPair updateDelta = OffsetPair( + final updateDelta = OffsetPair( local: localDelta, global: globalUpdateDelta, ); @@ -870,7 +871,7 @@ sealed class BaseTapAndDragGestureRecognizer return; } - final TapDragDownDetails details = TapDragDownDetails( + final details = TapDragDownDetails( globalPosition: event.position, localPosition: event.localPosition, kind: getKindForPointer(event.pointer), @@ -889,7 +890,7 @@ sealed class BaseTapAndDragGestureRecognizer return; } - final TapDragUpDetails upDetails = TapDragUpDetails( + final upDetails = TapDragUpDetails( kind: event.kind, globalPosition: event.position, localPosition: event.localPosition, @@ -908,7 +909,7 @@ sealed class BaseTapAndDragGestureRecognizer void _checkDragStart(PointerEvent event) { if (onDragStart != null) { - final TapDragStartDetails details = TapDragStartDetails( + final details = TapDragStartDetails( sourceTimeStamp: event.timeStamp, globalPosition: _initialPosition.global, localPosition: _initialPosition.local, @@ -926,7 +927,7 @@ sealed class BaseTapAndDragGestureRecognizer final Offset globalPosition = corrected?.global ?? event.position; final Offset localPosition = corrected?.local ?? event.localPosition; - final TapDragUpdateDetails details = TapDragUpdateDetails( + final details = TapDragUpdateDetails( sourceTimeStamp: event.timeStamp, delta: event.localDelta, globalPosition: globalPosition, @@ -962,7 +963,7 @@ sealed class BaseTapAndDragGestureRecognizer _handleDragUpdateThrottled(); } - final TapDragEndDetails endDetails = TapDragEndDetails( + final endDetails = TapDragEndDetails( globalPosition: globalPosition, localPosition: localPosition, primaryVelocity: 0.0, diff --git a/lib/common/widgets/flutter/selectable_text/text_selection.dart b/lib/common/widgets/flutter/selectable_text/text_selection.dart index 58e187852..794d2cd27 100644 --- a/lib/common/widgets/flutter/selectable_text/text_selection.dart +++ b/lib/common/widgets/flutter/selectable_text/text_selection.dart @@ -11,7 +11,7 @@ import 'package:flutter/gestures.dart' BaseTapAndDragGestureRecognizer, TapAndHorizontalDragGestureRecognizer, TapAndPanGestureRecognizer; -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart' hide TextSelectionGestureDetector; class CustomTextSelectionGestureDetectorBuilder extends TextSelectionGestureDetectorBuilder { @@ -310,8 +310,7 @@ class _TextSelectionGestureDetectorState @override Widget build(BuildContext context) { - final Map gestures = - {}; + final gestures = {}; gestures[TapGestureRecognizer] = GestureRecognizerFactoryWithHandlers( diff --git a/lib/common/widgets/flutter/text/paragraph.dart b/lib/common/widgets/flutter/text/paragraph.dart index fd7ddcfae..ff60f8c77 100644 --- a/lib/common/widgets/flutter/text/paragraph.dart +++ b/lib/common/widgets/flutter/text/paragraph.dart @@ -24,7 +24,7 @@ import 'dart:ui' import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; +import 'package:flutter/rendering.dart' hide RenderParagraph; import 'package:flutter/services.dart'; /// The start and end positions for a text boundary. diff --git a/lib/common/widgets/flutter/text/rich_text.dart b/lib/common/widgets/flutter/text/rich_text.dart index 7997393cb..3d82b9bcb 100644 --- a/lib/common/widgets/flutter/text/rich_text.dart +++ b/lib/common/widgets/flutter/text/rich_text.dart @@ -5,7 +5,7 @@ import 'dart:ui' as ui show TextHeightBehavior; import 'package:PiliPlus/common/widgets/flutter/text/paragraph.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart' hide RichText; import 'package:flutter/rendering.dart' hide RenderParagraph; /// A paragraph of rich text. @@ -118,8 +118,8 @@ class RichText extends MultiChildRenderObjectWidget { this.textHeightBehavior, this.selectionRegistrar, this.selectionColor, - this.onShowMore, required this.primary, + this.onShowMore, }) : assert(maxLines == null || maxLines > 0), assert(selectionRegistrar == null || selectionColor != null), assert( diff --git a/lib/common/widgets/flutter/text/text.dart b/lib/common/widgets/flutter/text/text.dart index 3579cdeb3..c58b97ff0 100644 --- a/lib/common/widgets/flutter/text/text.dart +++ b/lib/common/widgets/flutter/text/text.dart @@ -20,7 +20,7 @@ import 'dart:ui' as ui show TextHeightBehavior; import 'package:PiliPlus/common/widgets/flutter/text/paragraph.dart'; import 'package:PiliPlus/common/widgets/flutter/text/rich_text.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart' hide RichText; +import 'package:flutter/material.dart' hide Text, RichText; import 'package:flutter/rendering.dart' hide RenderParagraph; /// A run of text with a single style. @@ -180,8 +180,8 @@ class Text extends StatelessWidget { this.textWidthBasis, this.textHeightBehavior, this.selectionColor, - this.onShowMore, required this.primary, + this.onShowMore, }) : textSpan = null, assert( textScaler == null || textScaleFactor == null, @@ -219,8 +219,8 @@ class Text extends StatelessWidget { this.textWidthBasis, this.textHeightBehavior, this.selectionColor, - this.onShowMore, required this.primary, + this.onShowMore, }) : data = null, assert( textScaler == null || textScaleFactor == null, @@ -242,9 +242,19 @@ class Text extends StatelessWidget { /// If the style's "inherit" property is true, the style will be merged with /// the closest enclosing [DefaultTextStyle]. Otherwise, the style will /// replace the closest enclosing [DefaultTextStyle]. + /// + /// The user or platform may override this [style]'s [TextStyle.fontWeight], + /// [TextStyle.height], [TextStyle.letterSpacing], and [TextStyle.wordSpacing] + /// via a [MediaQuery] ancestor's [MediaQueryData.boldText], + /// [MediaQueryData.lineHeightScaleFactorOverride], + /// [MediaQueryData.letterSpacingOverride], and [MediaQueryData.wordSpacingOverride] + /// regardless of its [TextStyle.inherit] value. final TextStyle? style; /// {@macro flutter.painting.textPainter.strutStyle} + /// + /// The user or platform may override this [strutStyle]'s [StrutStyle.height] + /// via a [MediaQuery] ancestor's [MediaQueryData.lineHeightScaleFactorOverride]. final StrutStyle? strutStyle; /// How the text should be aligned horizontally. @@ -375,6 +385,30 @@ class Text extends StatelessWidget { const TextStyle(fontWeight: FontWeight.bold), ); } + // TODO(Renzo-Olivares): Investigate ways the framework can automatically + // apply MediaQueryData.paragraphSpacingOverride to its own text components. + // See: https://github.com/flutter/flutter/issues/177953 and https://github.com/flutter/flutter/issues/177408. + final double? lineHeightScaleFactor = + MediaQuery.maybeLineHeightScaleFactorOverrideOf(context); + final double? letterSpacing = MediaQuery.maybeLetterSpacingOverrideOf( + context, + ); + final double? wordSpacing = MediaQuery.maybeWordSpacingOverrideOf(context); + final TextSpan effectiveTextSpan = + _OverridingTextStyleTextSpanUtils.applyTextSpacingOverrides( + lineHeightScaleFactor: lineHeightScaleFactor, + letterSpacing: letterSpacing, + wordSpacing: wordSpacing, + textSpan: TextSpan( + style: effectiveTextStyle, + text: data, + locale: locale, + children: textSpan != null ? [textSpan!] : null, + ), + ); + final StrutStyle? effectiveStrutStyle = strutStyle?.merge( + StrutStyle(height: lineHeightScaleFactor), + ); final SelectionRegistrar? registrar = SelectionContainer.maybeOf(context); final TextScaler textScaler = switch ((this.textScaler, textScaleFactor)) { (final TextScaler textScaler, _) => textScaler, @@ -403,7 +437,7 @@ class Text extends StatelessWidget { defaultTextStyle.overflow, textScaler: textScaler, maxLines: maxLines ?? defaultTextStyle.maxLines, - strutStyle: strutStyle, + strutStyle: effectiveStrutStyle, textWidthBasis: textWidthBasis ?? defaultTextStyle.textWidthBasis, textHeightBehavior: textHeightBehavior ?? @@ -413,12 +447,7 @@ class Text extends StatelessWidget { selectionColor ?? DefaultSelectionStyle.of(context).selectionColor ?? DefaultSelectionStyle.defaultColor, - text: TextSpan( - style: effectiveTextStyle, - text: data, - locale: locale, - children: textSpan != null ? [textSpan!] : null, - ), + text: effectiveTextSpan, primary: primary, ), ); @@ -436,7 +465,7 @@ class Text extends StatelessWidget { defaultTextStyle.overflow, textScaler: textScaler, maxLines: maxLines ?? defaultTextStyle.maxLines, - strutStyle: strutStyle, + strutStyle: effectiveStrutStyle, textWidthBasis: textWidthBasis ?? defaultTextStyle.textWidthBasis, textHeightBehavior: textHeightBehavior ?? @@ -446,14 +475,9 @@ class Text extends StatelessWidget { selectionColor ?? DefaultSelectionStyle.of(context).selectionColor ?? DefaultSelectionStyle.defaultColor, - text: TextSpan( - style: effectiveTextStyle, - text: data, - locale: locale, - children: textSpan != null ? [textSpan!] : null, - ), - onShowMore: onShowMore, + text: effectiveTextSpan, primary: primary, + onShowMore: onShowMore, ); } if (semanticsLabel != null || semanticsIdentifier != null) { @@ -483,49 +507,50 @@ class Text extends StatelessWidget { ); } style?.debugFillProperties(properties); - properties.add( - EnumProperty('textAlign', textAlign, defaultValue: null), - ); - properties.add( - EnumProperty( - 'textDirection', - textDirection, - defaultValue: null, - ), - ); - properties.add( - DiagnosticsProperty('locale', locale, defaultValue: null), - ); - properties.add( - FlagProperty( - 'softWrap', - value: softWrap, - ifTrue: 'wrapping at box width', - ifFalse: 'no wrapping except at line break characters', - showName: true, - ), - ); - properties.add( - EnumProperty('overflow', overflow, defaultValue: null), - ); - properties.add( - DoubleProperty('textScaleFactor', textScaleFactor, defaultValue: null), - ); - properties.add(IntProperty('maxLines', maxLines, defaultValue: null)); - properties.add( - EnumProperty( - 'textWidthBasis', - textWidthBasis, - defaultValue: null, - ), - ); - properties.add( - DiagnosticsProperty( - 'textHeightBehavior', - textHeightBehavior, - defaultValue: null, - ), - ); + properties + ..add( + EnumProperty('textAlign', textAlign, defaultValue: null), + ) + ..add( + EnumProperty( + 'textDirection', + textDirection, + defaultValue: null, + ), + ) + ..add( + DiagnosticsProperty('locale', locale, defaultValue: null), + ) + ..add( + FlagProperty( + 'softWrap', + value: softWrap, + ifTrue: 'wrapping at box width', + ifFalse: 'no wrapping except at line break characters', + showName: true, + ), + ) + ..add( + EnumProperty('overflow', overflow, defaultValue: null), + ) + ..add( + DoubleProperty('textScaleFactor', textScaleFactor, defaultValue: null), + ) + ..add(IntProperty('maxLines', maxLines, defaultValue: null)) + ..add( + EnumProperty( + 'textWidthBasis', + textWidthBasis, + defaultValue: null, + ), + ) + ..add( + DiagnosticsProperty( + 'textHeightBehavior', + textHeightBehavior, + defaultValue: null, + ), + ); if (semanticsLabel != null) { properties.add(StringProperty('semanticsLabel', semanticsLabel)); } @@ -693,7 +718,7 @@ class _SelectableTextContainerDelegate SelectionResult _handleSelectParagraph(SelectParagraphSelectionEvent event) { if (event.absorb) { - for (int index = 0; index < selectables.length; index += 1) { + for (var index = 0; index < selectables.length; index += 1) { dispatchSelectionEventToChild(selectables[index], event); } currentSelectionStartIndex = 0; @@ -703,7 +728,7 @@ class _SelectableTextContainerDelegate // First pass, if the position is on a placeholder then dispatch the selection // event to the [Selectable] at the location and terminate. - for (int index = 0; index < selectables.length; index += 1) { + for (var index = 0; index < selectables.length; index += 1) { final bool selectableIsPlaceholder = !paragraph .selectableBelongsToParagraph(selectables[index]); if (selectableIsPlaceholder && @@ -722,9 +747,9 @@ class _SelectableTextContainerDelegate } SelectionResult? lastSelectionResult; - bool foundStart = false; + var foundStart = false; int? lastNextIndex; - for (int index = 0; index < selectables.length; index += 1) { + for (var index = 0; index < selectables.length; index += 1) { if (!paragraph.selectableBelongsToParagraph(selectables[index])) { if (foundStart) { final SelectionEvent synthesizedEvent = SelectParagraphSelectionEvent( @@ -769,7 +794,7 @@ class _SelectableTextContainerDelegate .overlaps( selectables[index].value.selectionRects[0], ); - int startIndex = 0; + var startIndex = 0; if (lastNextIndex != null && selectionAtStartOfSelectable) { startIndex = lastNextIndex + 1; } else { @@ -777,7 +802,7 @@ class _SelectableTextContainerDelegate ? 0 : index; } - for (int i = startIndex; i < index; i += 1) { + for (var i = startIndex; i < index; i += 1) { final SelectionEvent synthesizedEvent = SelectParagraphSelectionEvent( globalPosition: event.globalPosition, @@ -796,7 +821,7 @@ class _SelectableTextContainerDelegate if (selectables[index].value != existingGeometry) { if (!foundStart && lastNextIndex == null) { currentSelectionStartIndex = 0; - for (int i = 0; i < index; i += 1) { + for (var i = 0; i < index; i += 1) { final SelectionEvent synthesizedEvent = SelectParagraphSelectionEvent( globalPosition: event.globalPosition, @@ -837,7 +862,7 @@ class _SelectableTextContainerDelegate ); SelectionResult? finalResult; // Begin the search for the selection edge at the opposite edge if it exists. - final bool hasOppositeEdge = isEnd + final hasOppositeEdge = isEnd ? currentSelectionStartIndex != -1 : currentSelectionEndIndex != -1; int newIndex = switch ((isEnd, hasOppositeEdge)) { @@ -932,10 +957,10 @@ class _SelectableTextContainerDelegate // // This can happen when there is a scrollable child and the edge being adjusted // has been scrolled out of view. - final bool isCurrentEdgeWithinViewport = isEnd + final isCurrentEdgeWithinViewport = isEnd ? value.endSelectionPoint != null : value.startSelectionPoint != null; - final bool isOppositeEdgeWithinViewport = isEnd + final isOppositeEdgeWithinViewport = isEnd ? value.startSelectionPoint != null : value.endSelectionPoint != null; int newIndex = switch (( @@ -1107,9 +1132,9 @@ class _SelectableTextContainerDelegate if (currentSelectionStartIndex == -1 || currentSelectionEndIndex == -1) { return null; } - int startOffset = 0; - int endOffset = 0; - bool foundStart = false; + var startOffset = 0; + var endOffset = 0; + var foundStart = false; bool forwardSelection = currentSelectionEndIndex >= currentSelectionStartIndex; if (currentSelectionEndIndex == currentSelectionStartIndex) { @@ -1121,7 +1146,7 @@ class _SelectableTextContainerDelegate rangeAtSelectableInSelection.endOffset >= rangeAtSelectableInSelection.startOffset; } - for (int index = 0; index < selections.length; index++) { + for (var index = 0; index < selections.length; index++) { final _SelectionInfo selection = selections[index]; if (selection.range == null) { if (foundStart) { @@ -1187,7 +1212,7 @@ class _SelectableTextContainerDelegate /// this method will return `null`. @override SelectedContentRange? getSelection() { - final List<_SelectionInfo> selections = <_SelectionInfo>[ + final selections = <_SelectionInfo>[ for (final Selectable selectable in selectables) ( contentLength: selectable.contentLength, @@ -1232,7 +1257,7 @@ class _SelectableTextContainerDelegate currentSelectionStartIndex, currentSelectionEndIndex, ); - for (int index = 0; index < selectables.length; index += 1) { + for (var index = 0; index < selectables.length; index += 1) { if (index >= skipStart && index <= skipEnd) { continue; } @@ -1266,3 +1291,55 @@ class _SelectableTextContainerDelegate /// The length of the content that can be selected, and the range that is /// selected. typedef _SelectionInfo = ({int contentLength, SelectedContentRange? range}); + +/// A utility class for overriding the text styles of a [TextSpan] tree. +// When changes are made to this class, the equivalent API in editable_text.dart +// must also be updated. +// TODO(Renzo-Olivares): Remove after investigating a solution for overriding all +// styles for children in an [InlineSpan] tree, see: https://github.com/flutter/flutter/issues/177952. +class _OverridingTextStyleTextSpanUtils { + static TextSpan applyTextSpacingOverrides({ + double? lineHeightScaleFactor, + double? letterSpacing, + double? wordSpacing, + required TextSpan textSpan, + }) { + if (lineHeightScaleFactor == null && + letterSpacing == null && + wordSpacing == null) { + return textSpan; + } + return _applyTextStyleOverrides( + TextStyle( + height: lineHeightScaleFactor, + letterSpacing: letterSpacing, + wordSpacing: wordSpacing, + ), + textSpan, + ); + } + + static TextSpan _applyTextStyleOverrides( + TextStyle overrideTextStyle, + TextSpan textSpan, + ) { + return TextSpan( + text: textSpan.text, + children: textSpan.children?.map((InlineSpan child) { + if (child is TextSpan && child.runtimeType == TextSpan) { + return _applyTextStyleOverrides(overrideTextStyle, child); + } + return child; + }).toList(), + style: textSpan.style?.merge(overrideTextStyle) ?? overrideTextStyle, + recognizer: textSpan.recognizer, + mouseCursor: textSpan.mouseCursor, + onEnter: textSpan.onEnter, + onExit: textSpan.onExit, + semanticsLabel: textSpan.semanticsLabel, + semanticsIdentifier: textSpan.semanticsIdentifier, + locale: textSpan.locale, + spellOut: textSpan.spellOut, + ); + } +} diff --git a/lib/common/widgets/flutter/text_field/adaptive_text_selection_toolbar.dart b/lib/common/widgets/flutter/text_field/adaptive_text_selection_toolbar.dart index 215e42d3b..059145edf 100644 --- a/lib/common/widgets/flutter/text_field/adaptive_text_selection_toolbar.dart +++ b/lib/common/widgets/flutter/text_field/adaptive_text_selection_toolbar.dart @@ -273,8 +273,8 @@ class AdaptiveTextSelectionToolbar extends StatelessWidget { }); case TargetPlatform.fuchsia: case TargetPlatform.android: - final List buttons = []; - for (int i = 0; i < buttonItems.length; i++) { + final buttons = []; + for (var i = 0; i < buttonItems.length; i++) { final ContextMenuButtonItem buttonItem = buttonItems[i]; buttons.add( TextSelectionToolbarTextButton( diff --git a/lib/common/widgets/flutter/text_field/cupertino/spell_check_suggestions_toolbar.dart b/lib/common/widgets/flutter/text_field/cupertino/spell_check_suggestions_toolbar.dart index 3470103a8..429b5bdb5 100644 --- a/lib/common/widgets/flutter/text_field/cupertino/spell_check_suggestions_toolbar.dart +++ b/lib/common/widgets/flutter/text_field/cupertino/spell_check_suggestions_toolbar.dart @@ -90,7 +90,7 @@ class CupertinoSpellCheckSuggestionsToolbar extends StatelessWidget { ]; } - final List buttonItems = []; + final buttonItems = []; // Build suggestion buttons. for (final String suggestion in spanAtCursorIndex.suggestions.take( diff --git a/lib/common/widgets/flutter/text_field/cupertino/text_field.dart b/lib/common/widgets/flutter/text_field/cupertino/text_field.dart index 7e4de43aa..54d04c6d8 100644 --- a/lib/common/widgets/flutter/text_field/cupertino/text_field.dart +++ b/lib/common/widgets/flutter/text_field/cupertino/text_field.dart @@ -99,7 +99,7 @@ class _CupertinoTextFieldSelectionGestureDetectorBuilder // this handler. If the clear button widget recognizes the up event, // then do not handle it. if (_state._clearGlobalKey.currentContext != null) { - final RenderBox renderBox = + final renderBox = _state._clearGlobalKey.currentContext!.findRenderObject()! as RenderBox; final Offset localOffset = renderBox.globalToLocal( @@ -1482,6 +1482,7 @@ class _CupertinoRichTextFieldState extends State child: _BaselineAlignedStack( placeholder: placeholder, editableText: editableText, + textAlignVertical: _textAlignVertical, editableTextBaseline: textStyle.textBaseline ?? TextBaseline.alphabetic, placeholderBaseline: @@ -1555,11 +1556,11 @@ class _CupertinoRichTextFieldState extends State } final bool enabled = widget.enabled; - final Offset cursorOffset = Offset( + final cursorOffset = Offset( _iOSHorizontalCursorOffsetPixels / MediaQuery.devicePixelRatioOf(context), 0, ); - final List formatters = [ + final formatters = [ ...?widget.inputFormatters, if (widget.maxLength != null) LengthLimitingTextInputFormatter( @@ -1617,7 +1618,7 @@ class _CupertinoRichTextFieldState extends State ); final BoxBorder? border = widget.decoration?.border; - Border? resolvedBorder = border as Border?; + var resolvedBorder = border as Border?; if (border is Border) { BorderSide resolveBorderSide(BorderSide side) { return side == BorderSide.none @@ -1828,14 +1829,16 @@ class _BaselineAlignedStack const _BaselineAlignedStack({ required this.editableTextBaseline, required this.placeholderBaseline, + required this.textAlignVertical, required this.editableText, this.placeholder, }); final TextBaseline editableTextBaseline; final TextBaseline placeholderBaseline; - final Widget? placeholder; + final TextAlignVertical textAlignVertical; final Widget editableText; + final Widget? placeholder; @override Iterable<_BaselineAlignedStackSlot> get slots => @@ -1852,6 +1855,7 @@ class _BaselineAlignedStack @override _RenderBaselineAlignedStack createRenderObject(BuildContext context) { return _RenderBaselineAlignedStack( + textAlignVertical: textAlignVertical, editableTextBaseline: editableTextBaseline, placeholderBaseline: placeholderBaseline, ); @@ -1863,6 +1867,7 @@ class _BaselineAlignedStack _RenderBaselineAlignedStack renderObject, ) { renderObject + ..textAlignVertical = textAlignVertical ..editableTextBaseline = editableTextBaseline ..placeholderBaseline = placeholderBaseline; } @@ -1878,11 +1883,23 @@ class _RenderBaselineAlignedStack extends RenderBox RenderBox > { _RenderBaselineAlignedStack({ + required TextAlignVertical textAlignVertical, required TextBaseline editableTextBaseline, required TextBaseline placeholderBaseline, - }) : _editableTextBaseline = editableTextBaseline, + }) : _textAlignVertical = textAlignVertical, + _editableTextBaseline = editableTextBaseline, _placeholderBaseline = placeholderBaseline; + TextAlignVertical get textAlignVertical => _textAlignVertical; + TextAlignVertical _textAlignVertical; + set textAlignVertical(TextAlignVertical value) { + if (_textAlignVertical == value) { + return; + } + _textAlignVertical = value; + markNeedsLayout(); + } + TextBaseline get editableTextBaseline => _editableTextBaseline; TextBaseline _editableTextBaseline; set editableTextBaseline(TextBaseline value) { @@ -1960,9 +1977,9 @@ class _RenderBaselineAlignedStack extends RenderBox final RenderBox? placeholder = _placeholderChild; final RenderBox editableText = _editableTextChild; - final _BaselineAlignedStackParentData editableTextParentData = + final editableTextParentData = editableText.parentData! as _BaselineAlignedStackParentData; - final _BaselineAlignedStackParentData? placeholderParentData = + final placeholderParentData = placeholder?.parentData as _BaselineAlignedStackParentData?; size = _computeSize( @@ -1979,13 +1996,17 @@ class _RenderBaselineAlignedStack extends RenderBox ); assert(placeholder != null || placeholderBaselineValue == null); - final double placeholderY = placeholderBaselineValue != null - ? editableTextBaselineValue - placeholderBaselineValue - : 0.0; + final Offset baselineDiff = placeholderBaselineValue != null + ? Offset(0.0, editableTextBaselineValue - placeholderBaselineValue) + : Offset.zero; + final verticalAlignment = Alignment(0.0, textAlignVertical.y); - final double offsetYAdjustment = math.max(0, placeholderY); - editableTextParentData.offset = Offset(0, offsetYAdjustment); - placeholderParentData?.offset = Offset(0, placeholderY + offsetYAdjustment); + editableTextParentData.offset = verticalAlignment.alongOffset( + size - editableText.size as Offset, + ); + // Baseline-align the placeholder to the editable text. + placeholderParentData?.offset = + editableTextParentData.offset + baselineDiff; } @override @@ -1994,12 +2015,12 @@ class _RenderBaselineAlignedStack extends RenderBox final RenderBox editableText = _editableTextChild; if (placeholder != null) { - final _BaselineAlignedStackParentData placeholderParentData = + final placeholderParentData = placeholder.parentData! as _BaselineAlignedStackParentData; context.paintChild(placeholder, offset + placeholderParentData.offset); } - final _BaselineAlignedStackParentData editableTextParentData = + final editableTextParentData = editableText.parentData! as _BaselineAlignedStackParentData; context.paintChild(editableText, offset + editableTextParentData.offset); } @@ -2054,7 +2075,7 @@ class _RenderBaselineAlignedStack extends RenderBox height = math.max(height, editableTextSize.height); width = math.max(width, editableTextSize.width); - final Size size = Size(width, height); + final size = Size(width, height); assert(size.isFinite); return constraints.constrain(size); } @@ -2062,7 +2083,7 @@ class _RenderBaselineAlignedStack extends RenderBox @override bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { final RenderBox editableText = _editableTextChild; - final _BaselineAlignedStackParentData editableTextParentData = + final editableTextParentData = editableText.parentData! as _BaselineAlignedStackParentData; return result.addWithPaintOffset( diff --git a/lib/common/widgets/flutter/text_field/editable.dart b/lib/common/widgets/flutter/text_field/editable.dart index 67a85bbe0..6772e4bf6 100644 --- a/lib/common/widgets/flutter/text_field/editable.dart +++ b/lib/common/widgets/flutter/text_field/editable.dart @@ -145,17 +145,13 @@ class VerticalCaretMovementRun implements Iterator { } assert(lineNumber != _currentLine); - final Offset newOffset = Offset( + final newOffset = Offset( _currentOffset.dx, _lineMetrics[lineNumber].baseline, ); final TextPosition closestPosition = _editable._textPainter .getPositionForOffset(newOffset); - final MapEntry position = - MapEntry( - newOffset, - closestPosition, - ); + final position = MapEntry(newOffset, closestPosition); _positionCache[lineNumber] = position; return position; } @@ -419,8 +415,9 @@ class RenderEditable extends RenderBox ); if (_foregroundRenderObject == null) { - final _RenderEditableCustomPaint foregroundRenderObject = - _RenderEditableCustomPaint(painter: effectivePainter); + final foregroundRenderObject = _RenderEditableCustomPaint( + painter: effectivePainter, + ); adoptChild(foregroundRenderObject); _foregroundRenderObject = foregroundRenderObject; } else { @@ -452,8 +449,9 @@ class RenderEditable extends RenderBox ); if (_backgroundRenderObject == null) { - final _RenderEditableCustomPaint backgroundRenderObject = - _RenderEditableCustomPaint(painter: effectivePainter); + final backgroundRenderObject = _RenderEditableCustomPaint( + painter: effectivePainter, + ); adoptChild(backgroundRenderObject); _backgroundRenderObject = backgroundRenderObject; } else { @@ -714,7 +712,7 @@ class RenderEditable extends RenderBox // happens in paragraph.cc's layout and TextPainter's // _applyFloatingPointHack. Ideally, the rounding mismatch will be fixed and // this can be changed to be a strict check instead of an approximation. - const double visibleRegionSlop = 0.5; + const visibleRegionSlop = 0.5; _selectionStartInViewport.value = visibleRegion .inflate(visibleRegionSlop) .contains(startOffset + effectiveOffset); @@ -1356,9 +1354,9 @@ class RenderEditable extends RenderBox obscuringCharacter * plainText.length, ); } else { - final StringBuffer buffer = StringBuffer(); - int offset = 0; - final List attributes = []; + final buffer = StringBuffer(); + var offset = 0; + final attributes = []; for (final InlineSpanSemanticsInformation info in _semanticsInfo!) { final String label = info.semanticsLabel ?? info.text; for (final StringAttribute infoAttribute in info.stringAttributes) { @@ -1436,19 +1434,19 @@ class RenderEditable extends RenderBox Iterable children, ) { assert(_semanticsInfo != null && _semanticsInfo!.isNotEmpty); - final List newChildren = []; + final newChildren = []; TextDirection currentDirection = textDirection; Rect currentRect; - double ordinal = 0.0; - int start = 0; - int placeholderIndex = 0; - int childIndex = 0; + var ordinal = 0.0; + var start = 0; + var placeholderIndex = 0; + var childIndex = 0; RenderBox? child = firstChild; - final Map newChildCache = {}; + final newChildCache = {}; _cachedCombinedSemanticsInfos ??= combineSemanticsInfo(_semanticsInfo!); for (final InlineSpanSemanticsInformation info in _cachedCombinedSemanticsInfos!) { - final TextSelection selection = TextSelection( + final selection = TextSelection( baseOffset: start, extentOffset: start + info.text.length, ); @@ -1462,8 +1460,7 @@ class RenderEditable extends RenderBox .elementAt(childIndex) .isTagged(PlaceholderSpanIndexSemanticsTag(placeholderIndex))) { final SemanticsNode childNode = children.elementAt(childIndex); - final TextParentData parentData = - child!.parentData! as TextParentData; + final parentData = child!.parentData! as TextParentData; assert(parentData.offset != null); newChildren.add(childNode); childIndex += 1; @@ -1471,7 +1468,7 @@ class RenderEditable extends RenderBox child = childAfter(child!); placeholderIndex += 1; } else { - final TextDirection initialDirection = currentDirection; + final initialDirection = currentDirection; final List rects = _textPainter.getBoxesForSelection( selection, ); @@ -1500,7 +1497,7 @@ class RenderEditable extends RenderBox rect.right.ceilToDouble() + 4.0, rect.bottom.ceilToDouble() + 4.0, ); - final SemanticsConfiguration configuration = SemanticsConfiguration() + final configuration = SemanticsConfiguration() ..sortKey = OrdinalSortKey(ordinal++) ..textDirection = initialDirection ..attributedLabel = AttributedString( @@ -1538,7 +1535,7 @@ class RenderEditable extends RenderBox if (_cachedChildNodes?.isNotEmpty ?? false) { newChild = _cachedChildNodes!.remove(_cachedChildNodes!.keys.first)!; } else { - final UniqueKey key = UniqueKey(); + final key = UniqueKey(); newChild = SemanticsNode( key: key, showOnScreen: _createShowOnScreenFor(key), @@ -1980,8 +1977,8 @@ class RenderEditable extends RenderBox if (cachedValue != null) { return cachedValue; } - int count = 0; - for (int index = 0; index < text.length; index += 1) { + var count = 0; + for (var index = 0; index < text.length; index += 1) { switch (text.codeUnitAt(index)) { case 0x000A: // LF case 0x0085: // NEL @@ -2242,7 +2239,7 @@ class RenderEditable extends RenderBox extentOffset = isNormalized ? newOffset.endOffset : newOffset.startOffset; } - final TextSelection newSelection = TextSelection( + final newSelection = TextSelection( baseOffset: baseOffset, extentOffset: extentOffset, affinity: fromPosition.affinity, @@ -2591,12 +2588,12 @@ class RenderEditable extends RenderBox }; size = Size(width, constraints.constrainHeight(preferredHeight)); - final Size contentSize = Size( + final contentSize = Size( _textPainter.width + _caretMargin, _textPainter.height, ); - final BoxConstraints painterConstraints = BoxConstraints.tight(contentSize); + final painterConstraints = BoxConstraints.tight(contentSize); _foregroundRenderObject?.layout(painterConstraints); _backgroundRenderObject?.layout(painterConstraints); @@ -2656,7 +2653,7 @@ class RenderEditable extends RenderBox final double rightBound = math.min(size.width, _textPainter.width) + floatingCursorAddedMargin.right; - final Rect boundingRects = Rect.fromLTRB( + final boundingRects = Rect.fromLTRB( leftBound, topBound, rightBound, @@ -2780,7 +2777,7 @@ class RenderEditable extends RenderBox startPosition, Rect.zero, ); - for (final ui.LineMetrics lineMetrics in metrics) { + for (final lineMetrics in metrics) { if (lineMetrics.baseline > offset.dy) { return MapEntry( lineMetrics.lineNumber, @@ -3163,7 +3160,7 @@ class _TextHighlightPainter extends RenderEditablePainter { ) .toSet(); - for (final TextBox box in boxes) { + for (final box in boxes) { canvas.drawRect( box .toRect() @@ -3290,7 +3287,7 @@ class _CaretPainter extends RenderEditablePainter { if (radius == null) { canvas.drawRect(integralRect, caretPaint); } else { - final RRect caretRRect = RRect.fromRectAndRadius(integralRect, radius); + final caretRRect = RRect.fromRectAndRadius(integralRect, radius); canvas.drawRRect(caretRRect, caretPaint); } } diff --git a/lib/common/widgets/flutter/text_field/editable_text.dart b/lib/common/widgets/flutter/text_field/editable_text.dart index c138ce68b..d955de628 100644 --- a/lib/common/widgets/flutter/text_field/editable_text.dart +++ b/lib/common/widgets/flutter/text_field/editable_text.dart @@ -146,7 +146,7 @@ class _DiscreteKeyFrameSimulation extends Simulation { : assert(_keyFrames.isNotEmpty), assert(_keyFrames.last.time <= maxDuration), assert(() { - for (int i = 0; i < _keyFrames.length - 1; i += 1) { + for (var i = 0; i < _keyFrames.length - 1; i += 1) { if (_keyFrames[i].time > _keyFrames[i + 1].time) { return false; } @@ -407,7 +407,11 @@ class _DiscreteKeyFrameSimulation extends Simulation { /// ```dart /// onChanged: (String newText) { /// if (newText.isNotEmpty) { -/// SemanticsService.announce('\$$newText', Directionality.of(context)); +/// SemanticsService.sendAnnouncement( +/// View.of(context), +/// '\$$newText', +/// Directionality.of(context), +/// ); /// } /// } /// ``` @@ -692,6 +696,13 @@ class EditableText extends StatefulWidget { final bool enableSuggestions; /// The text style to use for the editable text. + /// + /// The user or platform may override this [style]'s [TextStyle.fontWeight], + /// [TextStyle.height], [TextStyle.letterSpacing], and [TextStyle.wordSpacing] + /// via a [MediaQuery] ancestor's [MediaQueryData.boldText], + /// [MediaQueryData.lineHeightScaleFactorOverride], + /// [MediaQueryData.letterSpacingOverride], and [MediaQueryData.wordSpacingOverride] + /// regardless of its [TextStyle.inherit] value. final TextStyle style; /// {@template flutter.widgets.editableText.strutStyle} @@ -718,6 +729,9 @@ class EditableText extends StatefulWidget { /// Within editable text and text fields, [StrutStyle] will not use its standalone /// default values, and will instead inherit omitted/null properties from the /// [TextStyle] instead. See [StrutStyle.inheritFromTextStyle]. + /// + /// The user or platform may override this [strutStyle]'s [StrutStyle.height] + /// via a [MediaQuery] ancestor's [MediaQueryData.lineHeightScaleFactorOverride]. StrutStyle get strutStyle { if (_strutStyle == null) { return StrutStyle.fromTextStyle(style, forceStrutHeight: true); @@ -1750,8 +1764,7 @@ class EditableText extends StatefulWidget { required final VoidCallback? onShare, required final VoidCallback? onLiveTextInput, }) { - final List resultButtonItem = - []; + final resultButtonItem = []; // Configure button items with clipboard. if (onPaste == null || clipboardStatus != ClipboardStatus.unknown) { @@ -1760,7 +1773,7 @@ class EditableText extends StatefulWidget { // shown. // On Android, the share button is before the select all button. - final bool showShareBeforeSelectAll = + final showShareBeforeSelectAll = defaultTargetPlatform == TargetPlatform.android; resultButtonItem.addAll([ @@ -1875,8 +1888,7 @@ class EditableText extends StatefulWidget { switch (defaultTargetPlatform) { case TargetPlatform.iOS: case TargetPlatform.macOS: - const Map - iOSKeyboardType = { + const iOSKeyboardType = { AutofillHints.addressCity: TextInputType.name, AutofillHints.addressCityAndState: TextInputType.name, // Autofill not working. @@ -1931,77 +1943,76 @@ class EditableText extends StatefulWidget { return TextInputType.multiline; } - const Map inferKeyboardType = - { - AutofillHints.addressCity: TextInputType.streetAddress, - AutofillHints.addressCityAndState: TextInputType.streetAddress, - AutofillHints.addressState: TextInputType.streetAddress, - AutofillHints.birthday: TextInputType.datetime, - AutofillHints.birthdayDay: TextInputType.datetime, - AutofillHints.birthdayMonth: TextInputType.datetime, - AutofillHints.birthdayYear: TextInputType.datetime, - AutofillHints.countryCode: TextInputType.number, - AutofillHints.countryName: TextInputType.text, - AutofillHints.creditCardExpirationDate: TextInputType.datetime, - AutofillHints.creditCardExpirationDay: TextInputType.datetime, - AutofillHints.creditCardExpirationMonth: TextInputType.datetime, - AutofillHints.creditCardExpirationYear: TextInputType.datetime, - AutofillHints.creditCardFamilyName: TextInputType.name, - AutofillHints.creditCardGivenName: TextInputType.name, - AutofillHints.creditCardMiddleName: TextInputType.name, - AutofillHints.creditCardName: TextInputType.name, - AutofillHints.creditCardNumber: TextInputType.number, - AutofillHints.creditCardSecurityCode: TextInputType.number, - AutofillHints.creditCardType: TextInputType.text, - AutofillHints.email: TextInputType.emailAddress, - AutofillHints.familyName: TextInputType.name, - AutofillHints.fullStreetAddress: TextInputType.streetAddress, - AutofillHints.gender: TextInputType.text, - AutofillHints.givenName: TextInputType.name, - AutofillHints.impp: TextInputType.url, - AutofillHints.jobTitle: TextInputType.text, - AutofillHints.language: TextInputType.text, - AutofillHints.location: TextInputType.streetAddress, - AutofillHints.middleInitial: TextInputType.name, - AutofillHints.middleName: TextInputType.name, - AutofillHints.name: TextInputType.name, - AutofillHints.namePrefix: TextInputType.name, - AutofillHints.nameSuffix: TextInputType.name, - AutofillHints.newPassword: TextInputType.text, - AutofillHints.newUsername: TextInputType.text, - AutofillHints.nickname: TextInputType.text, - AutofillHints.oneTimeCode: TextInputType.text, - AutofillHints.organizationName: TextInputType.text, - AutofillHints.password: TextInputType.text, - AutofillHints.photo: TextInputType.text, - AutofillHints.postalAddress: TextInputType.streetAddress, - AutofillHints.postalAddressExtended: TextInputType.streetAddress, - AutofillHints.postalAddressExtendedPostalCode: TextInputType.number, - AutofillHints.postalCode: TextInputType.number, - AutofillHints.streetAddressLevel1: TextInputType.streetAddress, - AutofillHints.streetAddressLevel2: TextInputType.streetAddress, - AutofillHints.streetAddressLevel3: TextInputType.streetAddress, - AutofillHints.streetAddressLevel4: TextInputType.streetAddress, - AutofillHints.streetAddressLine1: TextInputType.streetAddress, - AutofillHints.streetAddressLine2: TextInputType.streetAddress, - AutofillHints.streetAddressLine3: TextInputType.streetAddress, - AutofillHints.sublocality: TextInputType.streetAddress, - AutofillHints.telephoneNumber: TextInputType.phone, - AutofillHints.telephoneNumberAreaCode: TextInputType.phone, - AutofillHints.telephoneNumberCountryCode: TextInputType.phone, - AutofillHints.telephoneNumberDevice: TextInputType.phone, - AutofillHints.telephoneNumberExtension: TextInputType.phone, - AutofillHints.telephoneNumberLocal: TextInputType.phone, - AutofillHints.telephoneNumberLocalPrefix: TextInputType.phone, - AutofillHints.telephoneNumberLocalSuffix: TextInputType.phone, - AutofillHints.telephoneNumberNational: TextInputType.phone, - AutofillHints.transactionAmount: TextInputType.numberWithOptions( - decimal: true, - ), - AutofillHints.transactionCurrency: TextInputType.text, - AutofillHints.url: TextInputType.url, - AutofillHints.username: TextInputType.text, - }; + const inferKeyboardType = { + AutofillHints.addressCity: TextInputType.streetAddress, + AutofillHints.addressCityAndState: TextInputType.streetAddress, + AutofillHints.addressState: TextInputType.streetAddress, + AutofillHints.birthday: TextInputType.datetime, + AutofillHints.birthdayDay: TextInputType.datetime, + AutofillHints.birthdayMonth: TextInputType.datetime, + AutofillHints.birthdayYear: TextInputType.datetime, + AutofillHints.countryCode: TextInputType.number, + AutofillHints.countryName: TextInputType.text, + AutofillHints.creditCardExpirationDate: TextInputType.datetime, + AutofillHints.creditCardExpirationDay: TextInputType.datetime, + AutofillHints.creditCardExpirationMonth: TextInputType.datetime, + AutofillHints.creditCardExpirationYear: TextInputType.datetime, + AutofillHints.creditCardFamilyName: TextInputType.name, + AutofillHints.creditCardGivenName: TextInputType.name, + AutofillHints.creditCardMiddleName: TextInputType.name, + AutofillHints.creditCardName: TextInputType.name, + AutofillHints.creditCardNumber: TextInputType.number, + AutofillHints.creditCardSecurityCode: TextInputType.number, + AutofillHints.creditCardType: TextInputType.text, + AutofillHints.email: TextInputType.emailAddress, + AutofillHints.familyName: TextInputType.name, + AutofillHints.fullStreetAddress: TextInputType.streetAddress, + AutofillHints.gender: TextInputType.text, + AutofillHints.givenName: TextInputType.name, + AutofillHints.impp: TextInputType.url, + AutofillHints.jobTitle: TextInputType.text, + AutofillHints.language: TextInputType.text, + AutofillHints.location: TextInputType.streetAddress, + AutofillHints.middleInitial: TextInputType.name, + AutofillHints.middleName: TextInputType.name, + AutofillHints.name: TextInputType.name, + AutofillHints.namePrefix: TextInputType.name, + AutofillHints.nameSuffix: TextInputType.name, + AutofillHints.newPassword: TextInputType.text, + AutofillHints.newUsername: TextInputType.text, + AutofillHints.nickname: TextInputType.text, + AutofillHints.oneTimeCode: TextInputType.text, + AutofillHints.organizationName: TextInputType.text, + AutofillHints.password: TextInputType.text, + AutofillHints.photo: TextInputType.text, + AutofillHints.postalAddress: TextInputType.streetAddress, + AutofillHints.postalAddressExtended: TextInputType.streetAddress, + AutofillHints.postalAddressExtendedPostalCode: TextInputType.number, + AutofillHints.postalCode: TextInputType.number, + AutofillHints.streetAddressLevel1: TextInputType.streetAddress, + AutofillHints.streetAddressLevel2: TextInputType.streetAddress, + AutofillHints.streetAddressLevel3: TextInputType.streetAddress, + AutofillHints.streetAddressLevel4: TextInputType.streetAddress, + AutofillHints.streetAddressLine1: TextInputType.streetAddress, + AutofillHints.streetAddressLine2: TextInputType.streetAddress, + AutofillHints.streetAddressLine3: TextInputType.streetAddress, + AutofillHints.sublocality: TextInputType.streetAddress, + AutofillHints.telephoneNumber: TextInputType.phone, + AutofillHints.telephoneNumberAreaCode: TextInputType.phone, + AutofillHints.telephoneNumberCountryCode: TextInputType.phone, + AutofillHints.telephoneNumberDevice: TextInputType.phone, + AutofillHints.telephoneNumberExtension: TextInputType.phone, + AutofillHints.telephoneNumberLocal: TextInputType.phone, + AutofillHints.telephoneNumberLocalPrefix: TextInputType.phone, + AutofillHints.telephoneNumberLocalSuffix: TextInputType.phone, + AutofillHints.telephoneNumberNational: TextInputType.phone, + AutofillHints.transactionAmount: TextInputType.numberWithOptions( + decimal: true, + ), + AutofillHints.transactionCurrency: TextInputType.text, + AutofillHints.url: TextInputType.url, + AutofillHints.username: TextInputType.text, + }; return inferKeyboardType[effectiveHint] ?? TextInputType.text; } @@ -2341,7 +2352,7 @@ class EditableTextState extends State widget.cursorColor.alpha / 255.0, _cursorBlinkOpacityController.value, ); - return widget.cursorColor.withOpacity(effectiveOpacity); + return widget.cursorColor.withValues(alpha: effectiveOpacity); } @override @@ -2737,9 +2748,9 @@ class EditableTextState extends State final List suggestionSpans = spellCheckResults!.suggestionSpans; - int leftIndex = 0; + var leftIndex = 0; int rightIndex = suggestionSpans.length - 1; - int midIndex = 0; + var midIndex = 0; while (leftIndex <= rightIndex) { midIndex = ((leftIndex + rightIndex) / 2).floor(); @@ -2983,7 +2994,7 @@ class EditableTextState extends State } List get _textProcessingActionButtonItems { - final List buttonItems = []; + final buttonItems = []; final TextSelection selection = textEditingValue.selection; if (widget.obscureText || !selection.isValid || selection.isCollapsed) { return buttonItems; @@ -3033,17 +3044,29 @@ class EditableTextState extends State _spellCheckConfiguration = _inferSpellCheckConfiguration( widget.spellCheckConfiguration, ); - _appLifecycleListener = AppLifecycleListener( - onResume: () => _justResumed = true, - ); + _appLifecycleListener = AppLifecycleListener(onResume: _onResume); _initProcessTextActions(); } + void _onResume() { + _justResumed = true; + // To prevent adding multiple listeners, remove any existing one first. + FocusManager.instance.removeListener(_resetJustResumed); + // Reset _justResumed as soon as there is a focus change. + FocusManager.instance.addListener(_resetJustResumed); + } + + void _resetJustResumed() { + _justResumed = false; + FocusManager.instance.removeListener(_resetJustResumed); + } + /// Query the engine to initialize the list of text processing actions to show /// in the text selection toolbar. Future _initProcessTextActions() async { - _processTextActions.clear(); - _processTextActions.addAll(await _processTextService.queryTextActions()); + _processTextActions + ..clear() + ..addAll(await _processTextService.queryTextActions()); } // Whether `TickerMode.of(context)` is true and animations (like blinking the @@ -3269,11 +3292,13 @@ class EditableTextState extends State WidgetsBinding.instance.removeObserver(this); _liveTextInputStatus?.removeListener(_onChangedLiveTextInputStatus); _liveTextInputStatus?.dispose(); - clipboardStatus.removeListener(_onChangedClipboardStatus); - clipboardStatus.dispose(); + clipboardStatus + ..removeListener(_onChangedClipboardStatus) + ..dispose(); _cursorVisibilityNotifier.dispose(); _appLifecycleListener.dispose(); FocusManager.instance.removeListener(_unflagInternalFocus); + FocusManager.instance.removeListener(_resetJustResumed); _disposeScrollNotificationObserver(); super.dispose(); assert(_batchEditDepth <= 0, 'unfinished batch edits: $_batchEditDepth'); @@ -3803,7 +3828,7 @@ class EditableTextState extends State // The caret is vertically centered within the line. Expand the caret's // height so that it spans the line because we're going to ensure that the // entire expanded caret is scrolled into view. - final Rect expandedRect = Rect.fromCenter( + final expandedRect = Rect.fromCenter( center: rect.center, width: rect.width, height: math.max(rect.height, renderEditable.preferredLineHeight), @@ -4088,7 +4113,7 @@ class EditableTextState extends State view.devicePixelRatio; final double obscuredHorizontal = (view.padding.left + view.padding.right) / view.devicePixelRatio; - final Size visibleScreenSize = Size( + final visibleScreenSize = Size( screenSize.width - obscuredHorizontal, screenSize.height - obscuredVertical, ); @@ -4160,7 +4185,13 @@ class EditableTextState extends State _showToolbarOnScreenScheduled = true; SchedulerBinding.instance.addPostFrameCallback((Duration _) { _showToolbarOnScreenScheduled = false; - if (!mounted) { + if (!mounted || _dataWhenToolbarShowScheduled == null) { + return; + } + if (_dataWhenToolbarShowScheduled!.value != _value) { + // Value has changed so we should invalidate any toolbar scheduling. + _dataWhenToolbarShowScheduled = null; + _disposeScrollNotificationObserver(); return; } final Rect deviceRect = _calculateDeviceRect(); @@ -4210,7 +4241,7 @@ class EditableTextState extends State TextSelectionOverlay _createSelectionOverlay() { final EditableTextContextMenuBuilder? contextMenuBuilder = widget.contextMenuBuilder; - final TextSelectionOverlay selectionOverlay = TextSelectionOverlay( + final selectionOverlay = TextSelectionOverlay( controller: widget.controller, clipboardStatus: clipboardStatus, context: context, @@ -4319,7 +4350,7 @@ class EditableTextState extends State _showCaretOnScreenScheduled = false; // Since we are in a post frame callback, check currentContext in case // RenderEditable has been disposed (in which case it will be null). - final RenderEditable? renderEditable = + final renderEditable = _editableKey.currentContext?.findRenderObject() as RenderEditable?; if (renderEditable == null || !(renderEditable.selection?.isValid ?? false) || @@ -4435,13 +4466,30 @@ class EditableTextState extends State .spellCheckService! .fetchSpellCheckSuggestions(localeForSpellChecking!, text); - if (suggestions == null) { - // The request to fetch spell check suggestions was canceled due to ongoing request. + if (suggestions == null || !mounted) { + // The request to fetch spell check suggestions was canceled due to ongoing request, + // or the widget was unmounted. return; } spellCheckResults = SpellCheckResults(text, suggestions); - renderEditable.text = buildTextSpan(); + final double? lineHeightScaleFactor = + MediaQuery.maybeLineHeightScaleFactorOverrideOf( + context, + ); + final double? letterSpacing = MediaQuery.maybeLetterSpacingOverrideOf( + context, + ); + final double? wordSpacing = MediaQuery.maybeWordSpacingOverrideOf( + context, + ); + renderEditable.text = + _OverridingTextStyleTextSpanUtils.applyTextSpacingOverrides( + lineHeightScaleFactor: lineHeightScaleFactor, + letterSpacing: letterSpacing, + wordSpacing: wordSpacing, + textSpan: buildTextSpan(), + ); } catch (exception, stack) { FlutterError.reportError( FlutterErrorDetails( @@ -4461,10 +4509,10 @@ class EditableTextState extends State bool userInteraction = false, }) { final TextEditingValue oldValue = _value; - final bool textChanged = oldValue.text != value.text; + final textChanged = oldValue.text != value.text; final bool textCommitted = !oldValue.composing.isCollapsed && value.composing.isCollapsed; - final bool selectionChanged = oldValue.selection != value.selection; + final selectionChanged = oldValue.selection != value.selection; if (textChanged || textCommitted) { // Only apply input formatters if the text has changed (including uncommitted @@ -4567,8 +4615,8 @@ class EditableTextState extends State widget.cursorColor.alpha / 255.0, _cursorBlinkOpacityController.value, ); - renderEditable.cursorColor = widget.cursorColor.withOpacity( - effectiveOpacity, + renderEditable.cursorColor = widget.cursorColor.withValues( + alpha: effectiveOpacity, ); _cursorVisibilityNotifier.value = widget.showCursor && @@ -4797,6 +4845,8 @@ class EditableTextState extends State } final InlineSpan inlineSpan = renderEditable.text!; + final double? lineHeightScaleFactor = + MediaQuery.maybeLineHeightScaleFactorOverrideOf(context); final TextScaler effectiveTextScaler = switch (( widget.textScaler, widget.textScaleFactor, @@ -4808,7 +4858,7 @@ class EditableTextState extends State (null, null) => MediaQuery.textScalerOf(context), }; - final _ScribbleCacheKey newCacheKey = _ScribbleCacheKey( + final newCacheKey = _ScribbleCacheKey( inlineSpan: inlineSpan, textAlign: widget.textAlign, textDirection: _textDirection, @@ -4817,7 +4867,9 @@ class EditableTextState extends State widget.textHeightBehavior ?? DefaultTextHeightBehavior.maybeOf(context), locale: widget.locale, - structStyle: widget.strutStyle, + structStyle: widget.strutStyle.merge( + StrutStyle(height: lineHeightScaleFactor), + ), placeholder: _placeholderLocation, size: renderEditable.size, ); @@ -4830,14 +4882,14 @@ class EditableTextState extends State } _scribbleCacheKey = newCacheKey; - final List rects = []; - int graphemeStart = 0; + final rects = []; + var graphemeStart = 0; // Can't use _value.text here: the controller value could change between // frames. final String plainText = inlineSpan.toPlainText( includeSemanticsLabels: false, ); - final CharacterRange characterRange = CharacterRange(plainText); + final characterRange = CharacterRange(plainText); while (characterRange.moveNext()) { final int graphemeEnd = graphemeStart + characterRange.current.length; final List boxes = renderEditable.getBoxesForSelection( @@ -4911,9 +4963,7 @@ class EditableTextState extends State if (selection == null || !selection.isValid) { return; } - final TextPosition currentTextPosition = TextPosition( - offset: selection.start, - ); + final currentTextPosition = TextPosition(offset: selection.start); final Rect caretRect = renderEditable.getLocalRectForCaret( currentTextPosition, ); @@ -4942,7 +4992,7 @@ class EditableTextState extends State ) { // Compare the current TextEditingValue with the pre-format new // TextEditingValue value, in case the formatter would reject the change. - final bool shouldShowCaret = widget.readOnly + final shouldShowCaret = widget.readOnly ? _value.selection != value.selection : _value != value; if (shouldShowCaret) { @@ -5353,11 +5403,8 @@ class EditableTextState extends State final String text = _value.text; final TextSelection selection = _value.selection; - final bool atEnd = selection.baseOffset == text.length; - final CharacterRange transposing = CharacterRange.at( - text, - selection.baseOffset, - ); + final atEnd = selection.baseOffset == text.length; + final transposing = CharacterRange.at(text, selection.baseOffset); if (atEnd) { transposing.moveBack(2); } else { @@ -5459,8 +5506,7 @@ class EditableTextState extends State return; } - final ScrollableState? state = - _scrollableKey.currentState as ScrollableState?; + final state = _scrollableKey.currentState as ScrollableState?; final double increment = ScrollAction.getDirectionalIncrement( state!, intent, @@ -5487,8 +5533,7 @@ class EditableTextState extends State final Rect extentRect = renderEditable.getLocalRectForCaret( _value.selection.extent, ); - final ScrollableState? state = - _scrollableKey.currentState as ScrollableState?; + final state = _scrollableKey.currentState as ScrollableState?; final double increment = ScrollAction.getDirectionalIncrement( state!, ScrollIntent( @@ -5501,7 +5546,7 @@ class EditableTextState extends State if (_value.selection.extentOffset >= _value.text.length) { return; } - final Offset nextExtentOffset = Offset( + final nextExtentOffset = Offset( extentRect.left, extentRect.top + increment, ); @@ -5520,7 +5565,7 @@ class EditableTextState extends State if (_value.selection.extentOffset <= 0) { return; } - final Offset nextExtentOffset = Offset( + final nextExtentOffset = Offset( extentRect.left, extentRect.top + increment, ); @@ -5802,6 +5847,12 @@ class EditableTextState extends State ), (null, null) => MediaQuery.textScalerOf(context), }; + final double? lineHeightScaleFactor = + MediaQuery.maybeLineHeightScaleFactorOverrideOf(context); + final double? letterSpacing = MediaQuery.maybeLetterSpacingOverrideOf( + context, + ); + final double? wordSpacing = MediaQuery.maybeWordSpacingOverrideOf(context); final ui.SemanticsInputType inputType; switch (widget.keyboardType) { case TextInputType.phone: @@ -5892,7 +5943,14 @@ class EditableTextState extends State startHandleLayerLink: _startHandleLayerLink, endHandleLayerLink: _endHandleLayerLink, - inlineSpan: buildTextSpan(), + inlineSpan: + _OverridingTextStyleTextSpanUtils.applyTextSpacingOverrides( + lineHeightScaleFactor: + lineHeightScaleFactor, + letterSpacing: letterSpacing, + wordSpacing: wordSpacing, + textSpan: buildTextSpan(), + ), value: _value, cursorColor: _cursorColor, backgroundCursorColor: @@ -5904,7 +5962,11 @@ class EditableTextState extends State maxLines: widget.maxLines, minLines: widget.minLines, expands: widget.expands, - strutStyle: widget.strutStyle, + strutStyle: widget.strutStyle.merge( + StrutStyle( + height: lineHeightScaleFactor, + ), + ), selectionColor: _selectionOverlay ?.spellCheckToolbarIsVisible ?? @@ -5974,7 +6036,7 @@ class EditableTextState extends State String text = _value.text; text = widget.obscuringCharacter * text.length; // Reveal the latest character in an obscured field only on mobile. - const Set mobilePlatforms = { + const mobilePlatforms = { TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.iOS, @@ -5994,7 +6056,7 @@ class EditableTextState extends State } if (_placeholderLocation >= 0 && _placeholderLocation <= _value.text.length) { - final List<_ScribblePlaceholder> placeholders = <_ScribblePlaceholder>[]; + final placeholders = <_ScribblePlaceholder>[]; final int placeholderLocation = _value.text.length - _placeholderLocation; if (_isMultiline) { // The zero size placeholder here allows the line to break and keep the caret on the first line. @@ -6379,7 +6441,7 @@ class _ScribbleFocusableState extends State<_ScribbleFocusable> return false; } final Rect intersection = calculatedBounds.intersect(rect); - final HitTestResult result = HitTestResult(); + final result = HitTestResult(); WidgetsBinding.instance.hitTestInView( result, intersection.center, @@ -6392,7 +6454,7 @@ class _ScribbleFocusableState extends State<_ScribbleFocusable> @override Rect get bounds { - final RenderBox? box = context.findRenderObject() as RenderBox?; + final box = context.findRenderObject() as RenderBox?; if (box == null || !mounted || !box.attached) { return Rect.zero; } @@ -6422,7 +6484,7 @@ class _ScribblePlaceholder extends WidgetSpan { List? dimensions, }) { assert(debugAssertIsValid()); - final bool hasStyle = style != null; + final hasStyle = style != null; if (hasStyle) { builder.pushStyle(style!.getTextStyle(textScaler: textScaler)); } @@ -6539,13 +6601,13 @@ class _DeleteTextAction final TextBoundary atomicBoundary = state._characterBoundary(); if (!selection.isCollapsed) { // Expands the selection to ensure the range covers full graphemes. - final TextRange range = TextRange( + final range = TextRange( start: atomicBoundary.getLeadingTextBoundaryAt(selection.start) ?? state._value.text.length, end: atomicBoundary.getTrailingTextBoundaryAt(selection.end - 1) ?? 0, ); - final ReplaceTextIntent replaceTextIntent = ReplaceTextIntent( + final replaceTextIntent = ReplaceTextIntent( state._value, '', range, @@ -6571,7 +6633,7 @@ class _DeleteTextAction 0, extentOffset: target, ); - final ReplaceTextIntent replaceTextIntent = ReplaceTextIntent( + final replaceTextIntent = ReplaceTextIntent( state._value, '', rangeToDelete, @@ -6609,7 +6671,7 @@ class _UpdateTextSelectionAction // Returns true iff the given position is at a wordwrap boundary in the // upstream position. bool _isAtWordwrapUpstream(TextPosition position) { - final TextPosition end = TextPosition( + final end = TextPosition( offset: state.renderEditable.getLineAtOffset(position).end, affinity: TextAffinity.upstream, ); @@ -6622,7 +6684,7 @@ class _UpdateTextSelectionAction // Returns true if the given position at a wordwrap boundary in the // downstream position. bool _isAtWordwrapDownstream(TextPosition position) { - final TextPosition start = TextPosition( + final start = TextPosition( offset: state.renderEditable.getLineAtOffset(position).start, ); return start == position && @@ -6690,7 +6752,7 @@ class _UpdateTextSelectionAction (selection.baseOffset - selection.extentOffset) * (selection.baseOffset - newSelection.extentOffset) < 0; - final TextSelection newRange = shouldCollapseToBase + final newRange = shouldCollapseToBase ? TextSelection.fromPosition(selection.base) : newSelection; return Actions.invoke( @@ -6948,3 +7010,55 @@ class _EditableTextTapUpOutsideAction // The default action is a no-op. } } + +/// A utility class for overriding the text styles of a [TextSpan] tree. +// When changes are made to this class, the equivalent API in text.dart +// must also be updated. +// TODO(Renzo-Olivares): Remove after investigating a solution for overriding all +// styles for children in an [InlineSpan] tree, see: https://github.com/flutter/flutter/issues/177952. +class _OverridingTextStyleTextSpanUtils { + static TextSpan applyTextSpacingOverrides({ + double? lineHeightScaleFactor, + double? letterSpacing, + double? wordSpacing, + required TextSpan textSpan, + }) { + if (lineHeightScaleFactor == null && + letterSpacing == null && + wordSpacing == null) { + return textSpan; + } + return _applyTextStyleOverrides( + TextStyle( + height: lineHeightScaleFactor, + letterSpacing: letterSpacing, + wordSpacing: wordSpacing, + ), + textSpan, + ); + } + + static TextSpan _applyTextStyleOverrides( + TextStyle overrideTextStyle, + TextSpan textSpan, + ) { + return TextSpan( + text: textSpan.text, + children: textSpan.children?.map((InlineSpan child) { + if (child is TextSpan && child.runtimeType == TextSpan) { + return _applyTextStyleOverrides(overrideTextStyle, child); + } + return child; + }).toList(), + style: textSpan.style?.merge(overrideTextStyle) ?? overrideTextStyle, + recognizer: textSpan.recognizer, + mouseCursor: textSpan.mouseCursor, + onEnter: textSpan.onEnter, + onExit: textSpan.onExit, + semanticsLabel: textSpan.semanticsLabel, + semanticsIdentifier: textSpan.semanticsIdentifier, + locale: textSpan.locale, + spellOut: textSpan.spellOut, + ); + } +} diff --git a/lib/common/widgets/flutter/text_field/spell_check_suggestions_toolbar.dart b/lib/common/widgets/flutter/text_field/spell_check_suggestions_toolbar.dart index 8fabf49d3..549e93149 100644 --- a/lib/common/widgets/flutter/text_field/spell_check_suggestions_toolbar.dart +++ b/lib/common/widgets/flutter/text_field/spell_check_suggestions_toolbar.dart @@ -88,7 +88,7 @@ class SpellCheckSuggestionsToolbar extends StatelessWidget { return null; } - final List buttonItems = []; + final buttonItems = []; // Build suggestion buttons. for (final String suggestion in spanAtCursorIndex.suggestions.take( @@ -112,7 +112,7 @@ class SpellCheckSuggestionsToolbar extends StatelessWidget { } // Build delete button. - final ContextMenuButtonItem deleteButton = ContextMenuButtonItem( + final deleteButton = ContextMenuButtonItem( onPressed: () { if (!editableTextState.mounted) { return; @@ -174,18 +174,17 @@ class SpellCheckSuggestionsToolbar extends StatelessWidget { /// Builds the toolbar buttons based on the [buttonItems]. List _buildToolbarButtons(BuildContext context) { return buttonItems.map((ContextMenuButtonItem buttonItem) { - final TextSelectionToolbarTextButton button = - TextSelectionToolbarTextButton( - padding: const EdgeInsets.fromLTRB(20, 0, 0, 0), - onPressed: buttonItem.onPressed, - alignment: Alignment.centerLeft, - child: Text( - AdaptiveTextSelectionToolbar.getButtonLabel(context, buttonItem), - style: buttonItem.type == ContextMenuButtonType.delete - ? const TextStyle(color: Colors.blue) - : null, - ), - ); + final button = TextSelectionToolbarTextButton( + padding: const EdgeInsets.fromLTRB(20, 0, 0, 0), + onPressed: buttonItem.onPressed, + alignment: Alignment.centerLeft, + child: Text( + AdaptiveTextSelectionToolbar.getButtonLabel(context, buttonItem), + style: buttonItem.type == ContextMenuButtonType.delete + ? const TextStyle(color: Colors.blue) + : null, + ), + ); if (buttonItem.type != ContextMenuButtonType.delete) { return button; @@ -216,7 +215,7 @@ class SpellCheckSuggestionsToolbar extends StatelessWidget { mediaQueryData.padding.top + CupertinoTextSelectionToolbar.kToolbarScreenPadding; // Makes up for the Padding. - final Offset localAdjustment = Offset( + final localAdjustment = Offset( CupertinoTextSelectionToolbar.kToolbarScreenPadding, paddingAbove, ); diff --git a/lib/common/widgets/flutter/text_field/system_context_menu.dart b/lib/common/widgets/flutter/text_field/system_context_menu.dart index c5965d3a1..ed682f985 100644 --- a/lib/common/widgets/flutter/text_field/system_context_menu.dart +++ b/lib/common/widgets/flutter/text_field/system_context_menu.dart @@ -156,19 +156,37 @@ class SystemContextMenu extends StatefulWidget { static List getDefaultItems( EditableTextState editableTextState, ) { - return [ - if (editableTextState.copyEnabled) const IOSSystemContextMenuItemCopy(), - if (editableTextState.cutEnabled) const IOSSystemContextMenuItemCut(), - if (editableTextState.pasteEnabled) const IOSSystemContextMenuItemPaste(), - if (editableTextState.selectAllEnabled) - const IOSSystemContextMenuItemSelectAll(), - if (editableTextState.lookUpEnabled) - const IOSSystemContextMenuItemLookUp(), - if (editableTextState.searchWebEnabled) - const IOSSystemContextMenuItemSearchWeb(), - if (editableTextState.liveTextInputEnabled) - const IOSSystemContextMenuItemLiveText(), - ]; + final items = []; + + // Use the generic Flutter-rendered context menu model as the single source of truth. + for (final ContextMenuButtonItem button + in editableTextState.contextMenuButtonItems) { + switch (button.type) { + case ContextMenuButtonType.copy: + items.add(const IOSSystemContextMenuItemCopy()); + case ContextMenuButtonType.cut: + items.add(const IOSSystemContextMenuItemCut()); + case ContextMenuButtonType.paste: + items.add(const IOSSystemContextMenuItemPaste()); + case ContextMenuButtonType.selectAll: + items.add(const IOSSystemContextMenuItemSelectAll()); + case ContextMenuButtonType.lookUp: + items.add(const IOSSystemContextMenuItemLookUp()); + case ContextMenuButtonType.searchWeb: + items.add(const IOSSystemContextMenuItemSearchWeb()); + case ContextMenuButtonType.share: + items.add(const IOSSystemContextMenuItemShare()); + case ContextMenuButtonType.liveTextInput: + items.add(const IOSSystemContextMenuItemLiveText()); + case ContextMenuButtonType.delete: + // No native iOS system menu button for Delete — intentionally ignored. + case ContextMenuButtonType.custom: + // Custom items are provided explicitly via SystemContextMenu.items, + // not via defaults. Intentionally ignore in default mapping. + } + } + + return items; } @override diff --git a/lib/common/widgets/flutter/text_field/text_field.dart b/lib/common/widgets/flutter/text_field/text_field.dart index 61e797550..7cd3ed517 100644 --- a/lib/common/widgets/flutter/text_field/text_field.dart +++ b/lib/common/widgets/flutter/text_field/text_field.dart @@ -1302,14 +1302,17 @@ class RichTextFieldState extends State context, ); final ThemeData themeData = Theme.of(context); + final InputDecorationThemeData decorationTheme = InputDecorationTheme.of( + context, + ); final InputDecoration effectiveDecoration = (widget.decoration ?? const InputDecoration()) - .applyDefaults(themeData.inputDecorationTheme) + .applyDefaults(decorationTheme) .copyWith( enabled: _isEnabled, hintMaxLines: widget.decoration?.hintMaxLines ?? - themeData.inputDecorationTheme.hintMaxLines ?? + decorationTheme.hintMaxLines ?? widget.maxLines, ); @@ -1347,8 +1350,8 @@ class RichTextFieldState extends State return effectiveDecoration; } // No counter widget - String counterText = '$currentLength'; - String semanticCounterText = ''; + var counterText = '$currentLength'; + var semanticCounterText = ''; // Handle a real maxLength (positive number) if (widget.maxLength! > 0) { @@ -1655,7 +1658,7 @@ class RichTextFieldState extends State widget.keyboardAppearance ?? theme.brightness; final RichTextEditingController controller = _effectiveController; final FocusNode focusNode = _effectiveFocusNode; - final List formatters = [ + final formatters = [ ...?widget.inputFormatters, if (widget.maxLength != null) LengthLimitingTextInputFormatter( diff --git a/lib/common/widgets/flutter/vertical_tabs.dart b/lib/common/widgets/flutter/vertical_tabs.dart index 8d0c6f860..29d863813 100644 --- a/lib/common/widgets/flutter/vertical_tabs.dart +++ b/lib/common/widgets/flutter/vertical_tabs.dart @@ -1571,7 +1571,6 @@ class _VerticalTabBarState extends State { @override void didChangeDependencies() { super.didChangeDependencies(); - assert(debugCheckHasMaterial(context)); _updateTabController(); _initIndicatorPainter(); } diff --git a/lib/common/widgets/interactiveviewer_gallery/interactiveviewer_gallery.dart b/lib/common/widgets/interactiveviewer_gallery/interactiveviewer_gallery.dart index 27e41f08d..2e084e52c 100644 --- a/lib/common/widgets/interactiveviewer_gallery/interactiveviewer_gallery.dart +++ b/lib/common/widgets/interactiveviewer_gallery/interactiveviewer_gallery.dart @@ -87,7 +87,8 @@ class _InteractiveviewerGalleryState extends State late final _tween = Matrix4Tween(); late final _animatable = _tween.chain(CurveTween(curve: Curves.easeOut)); - late final ImageHorizontalDragGestureRecognizer _horizontalDragGestureRecognizer; + late final ImageHorizontalDragGestureRecognizer + _horizontalDragGestureRecognizer; late Offset _doubleTapLocalPosition; diff --git a/lib/pages/emote/view.dart b/lib/pages/emote/view.dart index ce1c5ad31..3be2ff6aa 100644 --- a/lib/pages/emote/view.dart +++ b/lib/pages/emote/view.dart @@ -189,28 +189,25 @@ class _EmotePanelState extends State ), ), Expanded( - child: Material( - type: MaterialType.transparency, - child: TabBar( - controller: _emotePanelController.tabController, - padding: const EdgeInsets.only(right: 60), - dividerColor: Colors.transparent, - dividerHeight: 0, - isScrollable: true, - tabs: response - .map( - (e) => Padding( - padding: const EdgeInsets.all(8), - child: NetworkImgLayer( - width: 24, - height: 24, - type: ImageType.emote, - src: e.url, - ), + child: TabBar( + controller: _emotePanelController.tabController, + padding: const EdgeInsets.only(right: 60), + dividerColor: Colors.transparent, + dividerHeight: 0, + isScrollable: true, + tabs: response + .map( + (e) => Padding( + padding: const EdgeInsets.all(8), + child: NetworkImgLayer( + width: 24, + height: 24, + type: ImageType.emote, + src: e.url, ), - ) - .toList(), - ), + ), + ) + .toList(), ), ), ], diff --git a/lib/pages/member_contribute/view.dart b/lib/pages/member_contribute/view.dart index 2e82de64b..6cf17a3e1 100644 --- a/lib/pages/member_contribute/view.dart +++ b/lib/pages/member_contribute/view.dart @@ -56,9 +56,7 @@ class _MemberContributeState extends State crossAxisAlignment: CrossAxisAlignment.start, children: [ TabBar( - overlayColor: const WidgetStatePropertyAll( - Colors.transparent, - ), + overlayColor: const WidgetStatePropertyAll(Colors.transparent), splashFactory: NoSplash.splashFactory, padding: const EdgeInsets.symmetric(horizontal: 8), isScrollable: true,