opt: exclude analysis flutter widget (#1745)

This commit is contained in:
My-Responsitories
2025-11-16 22:33:40 +08:00
committed by GitHub
parent 27ae296b28
commit 5ee83d902d
70 changed files with 108 additions and 113 deletions

View File

@@ -0,0 +1,462 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// 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/widgets.dart';
///
/// @docImport 'stack.dart';
library;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class CustomMultiChildLayout extends MultiChildRenderObjectWidget {
/// Creates a custom multi-child layout.
const CustomMultiChildLayout({
super.key,
required this.delegate,
super.children,
});
/// The delegate that controls the layout of the children.
final MultiChildLayoutDelegate delegate;
@override
RenderCustomMultiChildLayoutBox createRenderObject(BuildContext context) {
return RenderCustomMultiChildLayoutBox(delegate: delegate);
}
@override
void updateRenderObject(
BuildContext context,
RenderCustomMultiChildLayoutBox renderObject,
) {
renderObject.delegate = delegate;
}
}
/// A delegate that controls the layout of multiple children.
///
/// Used with [CustomMultiChildLayout] (in the widgets library) and
/// [RenderCustomMultiChildLayoutBox] (in the rendering library).
///
/// Delegates must be idempotent. Specifically, if two delegates are equal, then
/// they must produce the same layout. To change the layout, replace the
/// delegate with a different instance whose [shouldRelayout] returns true when
/// given the previous instance.
///
/// Override [getSize] to control the overall size of the layout. The size of
/// the layout cannot depend on layout properties of the children. This was
/// a design decision to simplify the delegate implementations: This way,
/// the delegate implementations do not have to also handle various intrinsic
/// sizing functions if the parent's size depended on the children.
/// If you want to build a custom layout where you define the size of that widget
/// based on its children, then you will have to create a custom render object.
/// See [MultiChildRenderObjectWidget] with [ContainerRenderObjectMixin] and
/// [RenderBoxContainerDefaultsMixin] to get started or [RenderStack] for an
/// example implementation.
///
/// Override [performLayout] to size and position the children. An
/// implementation of [performLayout] must call [layoutChild] exactly once for
/// each child, but it may call [layoutChild] on children in an arbitrary order.
/// Typically a delegate will use the size returned from [layoutChild] on one
/// child to determine the constraints for [performLayout] on another child or
/// to determine the offset for [positionChild] for that child or another child.
///
/// Override [shouldRelayout] to determine when the layout of the children needs
/// to be recomputed when the delegate changes.
///
/// The most efficient way to trigger a relayout is to supply a `relayout`
/// argument to the constructor of the [MultiChildLayoutDelegate]. The custom
/// layout will listen to this value and relayout whenever the Listenable
/// notifies its listeners, such as when an [Animation] ticks. This allows
/// the custom layout to avoid the build phase of the pipeline.
///
/// Each child must be wrapped in a [LayoutId] widget to assign the id that
/// identifies it to the delegate. The [LayoutId.id] needs to be unique among
/// the children that the [CustomMultiChildLayout] manages.
///
/// {@tool snippet}
///
/// Below is an example implementation of [performLayout] that causes one widget
/// (the follower) to be the same size as another (the leader):
///
/// ```dart
/// // Define your own slot numbers, depending upon the id assigned by LayoutId.
/// // Typical usage is to define an enum like the one below, and use those
/// // values as the ids.
/// enum _Slot {
/// leader,
/// follower,
/// }
///
/// class FollowTheLeader extends MultiChildLayoutDelegate {
/// @override
/// void performLayout(Size size) {
/// Size leaderSize = Size.zero;
///
/// if (hasChild(_Slot.leader)) {
/// leaderSize = layoutChild(_Slot.leader, BoxConstraints.loose(size));
/// positionChild(_Slot.leader, Offset.zero);
/// }
///
/// if (hasChild(_Slot.follower)) {
/// layoutChild(_Slot.follower, BoxConstraints.tight(leaderSize));
/// positionChild(_Slot.follower, Offset(size.width - leaderSize.width,
/// size.height - leaderSize.height));
/// }
/// }
///
/// @override
/// bool shouldRelayout(MultiChildLayoutDelegate oldDelegate) => false;
/// }
/// ```
/// {@end-tool}
///
/// The delegate gives the leader widget loose constraints, which means the
/// child determines what size to be (subject to fitting within the given size).
/// The delegate then remembers the size of that child and places it in the
/// upper left corner.
///
/// The delegate then gives the follower widget tight constraints, forcing it to
/// match the size of the leader widget. The delegate then places the follower
/// widget in the bottom right corner.
///
/// The leader and follower widget will paint in the order they appear in the
/// child list, regardless of the order in which [layoutChild] is called on
/// them.
///
/// See also:
///
/// * [CustomMultiChildLayout], the widget that uses this delegate.
/// * [RenderCustomMultiChildLayoutBox], render object that uses this
/// delegate.
abstract class MultiChildLayoutDelegate {
/// Creates a layout delegate.
///
/// The layout will update whenever [relayout] notifies its listeners.
MultiChildLayoutDelegate({Listenable? relayout}) : _relayout = relayout;
final Listenable? _relayout;
Map<Object, RenderBox>? _idToChild;
Set<RenderBox>? _debugChildrenNeedingLayout;
/// True if a non-null LayoutChild was provided for the specified id.
///
/// Call this from the [performLayout] method to determine which children
/// are available, if the child list might vary.
///
/// This method cannot be called from [getSize] as the size is not allowed
/// to depend on the children.
bool hasChild(Object childId) => _idToChild![childId] != null;
/// Ask the child to update its layout within the limits specified by
/// the constraints parameter. The child's size is returned.
///
/// Call this from your [performLayout] function to lay out each
/// child. Every child must be laid out using this function exactly
/// once each time the [performLayout] function is called.
Size layoutChild(Object childId, BoxConstraints constraints) {
final RenderBox? child = _idToChild![childId];
assert(() {
if (child == null) {
throw FlutterError(
'The $this custom multichild layout delegate tried to lay out a non-existent child.\n'
'There is no child with the id "$childId".',
);
}
if (!_debugChildrenNeedingLayout!.remove(child)) {
throw FlutterError(
'The $this custom multichild layout delegate tried to lay out the child with id "$childId" more than once.\n'
'Each child must be laid out exactly once.',
);
}
try {
assert(constraints.debugAssertIsValid(isAppliedConstraint: true));
} on AssertionError catch (exception) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary(
'The $this custom multichild layout delegate provided invalid box constraints for the child with id "$childId".',
),
DiagnosticsProperty<AssertionError>(
'Exception',
exception,
showName: false,
),
ErrorDescription(
'The minimum width and height must be greater than or equal to zero.\n'
'The maximum width must be greater than or equal to the minimum width.\n'
'The maximum height must be greater than or equal to the minimum height.',
),
]);
}
return true;
}());
child!.layout(constraints, parentUsesSize: true);
return child.size;
}
/// Specify the child's origin relative to this origin.
///
/// Call this from your [performLayout] function to position each
/// child. If you do not call this for a child, its position will
/// remain unchanged. Children initially have their position set to
/// (0,0), i.e. the top left of the [RenderCustomMultiChildLayoutBox].
void positionChild(Object childId, Offset offset) {
final RenderBox? child = _idToChild![childId];
assert(() {
if (child == null) {
throw FlutterError(
'The $this custom multichild layout delegate tried to position out a non-existent child:\n'
'There is no child with the id "$childId".',
);
}
return true;
}());
final MultiChildLayoutParentData childParentData =
child!.parentData! as MultiChildLayoutParentData;
childParentData.offset = offset;
}
DiagnosticsNode _debugDescribeChild(RenderBox child) {
final MultiChildLayoutParentData childParentData =
child.parentData! as MultiChildLayoutParentData;
return DiagnosticsProperty<RenderBox>('${childParentData.id}', child);
}
void _callPerformLayout(Size size, RenderBox? firstChild) {
// A particular layout delegate could be called reentrantly, e.g. if it used
// by both a parent and a child. So, we must restore the _idToChild map when
// we return.
final Map<Object, RenderBox>? previousIdToChild = _idToChild;
Set<RenderBox>? debugPreviousChildrenNeedingLayout;
assert(() {
debugPreviousChildrenNeedingLayout = _debugChildrenNeedingLayout;
_debugChildrenNeedingLayout = <RenderBox>{};
return true;
}());
try {
_idToChild = <Object, RenderBox>{};
RenderBox? child = firstChild;
while (child != null) {
final MultiChildLayoutParentData childParentData =
child.parentData! as MultiChildLayoutParentData;
assert(() {
if (childParentData.id == null) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary(
'Every child of a RenderCustomMultiChildLayoutBox must have an ID in its parent data.',
),
child!.describeForError('The following child has no ID'),
]);
}
return true;
}());
_idToChild![childParentData.id!] = child;
assert(() {
_debugChildrenNeedingLayout!.add(child!);
return true;
}());
child = childParentData.nextSibling;
}
performLayout(size);
assert(() {
if (_debugChildrenNeedingLayout!.isNotEmpty) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('Each child must be laid out exactly once.'),
DiagnosticsBlock(
name:
'The $this custom multichild layout delegate forgot '
'to lay out the following '
'${_debugChildrenNeedingLayout!.length > 1 ? 'children' : 'child'}',
properties: _debugChildrenNeedingLayout!
.map<DiagnosticsNode>(_debugDescribeChild)
.toList(),
),
]);
}
return true;
}());
} finally {
_idToChild = previousIdToChild;
assert(() {
_debugChildrenNeedingLayout = debugPreviousChildrenNeedingLayout;
return true;
}());
}
}
/// Override this method to return the size of this object given the
/// incoming constraints.
///
/// The size cannot reflect the sizes of the children. If this layout has a
/// fixed width or height the returned size can reflect that; the size will be
/// constrained to the given constraints.
///
/// By default, attempts to size the box to the biggest size
/// possible given the constraints.
Size getSize(BoxConstraints constraints) => constraints.biggest;
/// Override this method to lay out and position all children given this
/// widget's size.
///
/// This method must call [layoutChild] for each child. It should also specify
/// the final position of each child with [positionChild].
void performLayout(Size size);
/// Override this method to return true when the children need to be
/// laid out.
///
/// This should compare the fields of the current delegate and the given
/// `oldDelegate` and return true if the fields are such that the layout would
/// be different.
bool shouldRelayout(covariant MultiChildLayoutDelegate oldDelegate);
/// Override this method to include additional information in the
/// debugging data printed by [debugDumpRenderTree] and friends.
///
/// By default, returns the [runtimeType] of the class.
@override
String toString() => objectRuntimeType(this, 'MultiChildLayoutDelegate');
}
/// Defers the layout of multiple children to a delegate.
///
/// The delegate can determine the layout constraints for each child and can
/// decide where to position each child. The delegate can also determine the
/// size of the parent, but the size of the parent cannot depend on the sizes of
/// the children.
class RenderCustomMultiChildLayoutBox extends RenderBox
with
ContainerRenderObjectMixin<RenderBox, MultiChildLayoutParentData>,
RenderBoxContainerDefaultsMixin<RenderBox, MultiChildLayoutParentData> {
/// Creates a render object that customizes the layout of multiple children.
RenderCustomMultiChildLayoutBox({
List<RenderBox>? children,
required MultiChildLayoutDelegate delegate,
}) : _delegate = delegate {
addAll(children);
}
@override
void setupParentData(RenderBox child) {
if (child.parentData is! MultiChildLayoutParentData) {
child.parentData = MultiChildLayoutParentData();
}
}
/// The delegate that controls the layout of the children.
MultiChildLayoutDelegate get delegate => _delegate;
MultiChildLayoutDelegate _delegate;
set delegate(MultiChildLayoutDelegate newDelegate) {
if (_delegate == newDelegate) {
return;
}
final MultiChildLayoutDelegate oldDelegate = _delegate;
if (newDelegate.runtimeType != oldDelegate.runtimeType ||
newDelegate.shouldRelayout(oldDelegate)) {
markNeedsLayout();
}
_delegate = newDelegate;
if (attached) {
oldDelegate._relayout?.removeListener(markNeedsLayout);
newDelegate._relayout?.addListener(markNeedsLayout);
}
}
@override
void attach(PipelineOwner owner) {
super.attach(owner);
_delegate._relayout?.addListener(markNeedsLayout);
}
@override
void detach() {
_delegate._relayout?.removeListener(markNeedsLayout);
super.detach();
}
Size _getSize(BoxConstraints constraints) {
assert(constraints.debugAssertIsValid());
return constraints.constrain(_delegate.getSize(constraints));
}
// TODO(ianh): It's a bit dubious to be using the getSize function from the delegate to
// figure out the intrinsic dimensions. We really should either not support intrinsics,
// or we should expose intrinsic delegate callbacks and throw if they're not implemented.
@override
double computeMinIntrinsicWidth(double height) {
final double width = _getSize(
BoxConstraints.tightForFinite(height: height),
).width;
if (width.isFinite) {
return width;
}
return 0.0;
}
@override
double computeMaxIntrinsicWidth(double height) {
final double width = _getSize(
BoxConstraints.tightForFinite(height: height),
).width;
if (width.isFinite) {
return width;
}
return 0.0;
}
@override
double computeMinIntrinsicHeight(double width) {
final double height = _getSize(
BoxConstraints.tightForFinite(width: width),
).height;
if (height.isFinite) {
return height;
}
return 0.0;
}
@override
double computeMaxIntrinsicHeight(double width) {
final double height = _getSize(
BoxConstraints.tightForFinite(width: width),
).height;
if (height.isFinite) {
return height;
}
return 0.0;
}
@override
@protected
Size computeDryLayout(covariant BoxConstraints constraints) {
return _getSize(constraints);
}
@override
void performLayout() {
size = _getSize(constraints);
delegate._callPerformLayout(size, firstChild);
}
@override
void paint(PaintingContext context, Offset offset) {
defaultPaint(context, offset);
}
@override
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
return defaultHitTestChildren(result, position: position);
}
@override
bool get isRepaintBoundary => true;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,789 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// 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 'elevated_button_theme.dart';
/// @docImport 'menu_anchor.dart';
/// @docImport 'text_button_theme.dart';
/// @docImport 'text_theme.dart';
/// @docImport 'theme.dart';
library;
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/rendering.dart';
/// The base [StatefulWidget] class for buttons whose style is defined by a [ButtonStyle] object.
///
/// Concrete subclasses must override [defaultStyleOf] and [themeStyleOf].
///
/// See also:
/// * [ElevatedButton], a filled button whose material elevates when pressed.
/// * [FilledButton], a filled button that doesn't elevate when pressed.
/// * [FilledButton.tonal], a filled button variant that uses a secondary fill color.
/// * [OutlinedButton], a button with an outlined border and no fill color.
/// * [TextButton], a button with no outline or fill color.
/// * <https://m3.material.io/components/buttons/overview>, an overview of each of
/// the Material Design button types and how they should be used in designs.
abstract class ButtonStyleButton extends StatefulWidget {
/// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
const ButtonStyleButton({
super.key,
required this.onPressed,
required this.onLongPress,
required this.onHover,
required this.onFocusChange,
required this.style,
required this.focusNode,
required this.autofocus,
required this.clipBehavior,
this.statesController,
this.isSemanticButton = true,
@Deprecated(
'Remove this parameter as it is now ignored. '
'Use ButtonStyle.iconAlignment instead. '
'This feature was deprecated after v3.28.0-1.0.pre.',
)
this.iconAlignment,
this.tooltip,
required this.child,
});
/// Called when the button is tapped or otherwise activated.
///
/// If this callback and [onLongPress] are null, then the button will be disabled.
///
/// See also:
///
/// * [enabled], which is true if the button is enabled.
final VoidCallback? onPressed;
/// Called when the button is long-pressed.
///
/// If this callback and [onPressed] are null, then the button will be disabled.
///
/// See also:
///
/// * [enabled], which is true if the button is enabled.
final VoidCallback? onLongPress;
/// Called when a pointer enters or exits the button response area.
///
/// The value passed to the callback is true if a pointer has entered this
/// part of the material and false if a pointer has exited this part of the
/// material.
final ValueChanged<bool>? onHover;
/// Handler called when the focus changes.
///
/// Called with true if this widget's node gains focus, and false if it loses
/// focus.
final ValueChanged<bool>? onFocusChange;
/// Customizes this button's appearance.
///
/// Non-null properties of this style override the corresponding
/// properties in [themeStyleOf] and [defaultStyleOf]. [WidgetStateProperty]s
/// that resolve to non-null values will similarly override the corresponding
/// [WidgetStateProperty]s in [themeStyleOf] and [defaultStyleOf].
///
/// Null by default.
final ButtonStyle? style;
/// {@macro flutter.material.Material.clipBehavior}
///
/// Defaults to [Clip.none] unless [ButtonStyle.backgroundBuilder] or
/// [ButtonStyle.foregroundBuilder] is specified. In those
/// cases the default is [Clip.antiAlias].
final Clip? clipBehavior;
/// {@macro flutter.widgets.Focus.focusNode}
final FocusNode? focusNode;
/// {@macro flutter.widgets.Focus.autofocus}
final bool autofocus;
/// {@macro flutter.material.inkwell.statesController}
final WidgetStatesController? statesController;
/// Determine whether this subtree represents a button.
///
/// If this is null, the screen reader will not announce "button" when this
/// is focused. This is useful for [MenuItemButton] and [SubmenuButton] when we
/// traverse the menu system.
///
/// Defaults to true.
final bool? isSemanticButton;
/// {@macro flutter.material.ButtonStyleButton.iconAlignment}
@Deprecated(
'Remove this parameter as it is now ignored. '
'Use ButtonStyle.iconAlignment instead. '
'This feature was deprecated after v3.28.0-1.0.pre.',
)
final IconAlignment? iconAlignment;
/// Text that describes the action that will occur when the button is pressed or
/// hovered over.
///
/// This text is displayed when the user long-presses or hovers over the button
/// in a tooltip. This string is also used for accessibility.
///
/// If null, the button will not display a tooltip.
final String? tooltip;
/// Typically the button's label.
///
/// {@macro flutter.widgets.ProxyWidget.child}
final Widget? child;
/// Returns a [ButtonStyle] that's based primarily on the [Theme]'s
/// [ThemeData.textTheme] and [ThemeData.colorScheme], but has most values
/// filled out (non-null).
///
/// The returned style can be overridden by the [style] parameter and by the
/// style returned by [themeStyleOf] that some button-specific themes like
/// [TextButtonTheme] or [ElevatedButtonTheme] override. For example the
/// default style of the [TextButton] subclass can be overridden with its
/// [TextButton.style] constructor parameter, or with a [TextButtonTheme].
///
/// Concrete button subclasses should return a [ButtonStyle] with as many
/// non-null properties as possible, where all of the non-null
/// [WidgetStateProperty] properties resolve to non-null values.
///
/// ## Properties that can be null
///
/// Some properties, like [ButtonStyle.fixedSize] would override other values
/// in the same [ButtonStyle] if set, so they are allowed to be null. Here is
/// a summary of properties that are allowed to be null when returned in the
/// [ButtonStyle] returned by this function, an why:
///
/// - [ButtonStyle.fixedSize] because it would override other values in the
/// same [ButtonStyle], like [ButtonStyle.maximumSize].
/// - [ButtonStyle.side] because null is a valid value for a button that has
/// no side. [OutlinedButton] returns a non-null default for this, however.
/// - [ButtonStyle.backgroundBuilder] and [ButtonStyle.foregroundBuilder]
/// because they would override the [ButtonStyle.foregroundColor] and
/// [ButtonStyle.backgroundColor] of the same [ButtonStyle].
///
/// See also:
///
/// * [themeStyleOf], returns the ButtonStyle of this button's component
/// theme.
@protected
ButtonStyle defaultStyleOf(BuildContext context);
/// Returns the ButtonStyle that belongs to the button's component theme.
///
/// The returned style can be overridden by the [style] parameter.
///
/// Concrete button subclasses should return the ButtonStyle for the
/// nearest subclass-specific inherited theme, and if no such theme
/// exists, then the same value from the overall [Theme].
///
/// See also:
///
/// * [defaultStyleOf], Returns the default [ButtonStyle] for this button.
@protected
ButtonStyle? themeStyleOf(BuildContext context);
/// Whether the button is enabled or disabled.
///
/// Buttons are disabled by default. To enable a button, set its [onPressed]
/// or [onLongPress] properties to a non-null value.
bool get enabled => onPressed != null || onLongPress != null;
@override
State<ButtonStyleButton> createState() => _ButtonStyleState();
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(
FlagProperty('enabled', value: enabled, ifFalse: 'disabled'),
)
..add(
DiagnosticsProperty<ButtonStyle>('style', style, defaultValue: null),
)
..add(
DiagnosticsProperty<FocusNode>(
'focusNode',
focusNode,
defaultValue: null,
),
);
}
/// Returns null if [value] is null, otherwise `WidgetStatePropertyAll<T>(value)`.
///
/// A convenience method for subclasses.
static WidgetStateProperty<T>? allOrNull<T>(T? value) =>
value == null ? null : WidgetStatePropertyAll<T>(value);
/// Returns null if [enabled] and [disabled] are null.
/// Otherwise, returns a [WidgetStateProperty] that resolves to [disabled]
/// when [WidgetState.disabled] is active, and [enabled] otherwise.
///
/// A convenience method for subclasses.
static WidgetStateProperty<Color?>? defaultColor(
Color? enabled,
Color? disabled,
) {
if ((enabled ?? disabled) == null) {
return null;
}
return WidgetStateProperty<Color?>.fromMap(<WidgetStatesConstraint, Color?>{
WidgetState.disabled: disabled,
WidgetState.any: enabled,
});
}
/// A convenience method used by subclasses in the framework, that returns an
/// interpolated value based on the [fontSizeMultiplier] parameter:
///
/// * 0 - 1 [geometry1x]
/// * 1 - 2 lerp([geometry1x], [geometry2x], [fontSizeMultiplier] - 1)
/// * 2 - 3 lerp([geometry2x], [geometry3x], [fontSizeMultiplier] - 2)
/// * otherwise [geometry3x]
///
/// This method is used by the framework for estimating the default paddings to
/// use on a button with a text label, when the system text scaling setting
/// changes. It's usually supplied with empirical [geometry1x], [geometry2x],
/// [geometry3x] values adjusted for different system text scaling values, when
/// the unscaled font size is set to 14.0 (the default [TextTheme.labelLarge]
/// value).
///
/// The `fontSizeMultiplier` argument, for historical reasons, is the default
/// font size specified in the [ButtonStyle], scaled by the ambient font
/// scaler, then divided by 14.0 (the default font size used in buttons).
static EdgeInsetsGeometry scaledPadding(
EdgeInsetsGeometry geometry1x,
EdgeInsetsGeometry geometry2x,
EdgeInsetsGeometry geometry3x,
double fontSizeMultiplier,
) {
return switch (fontSizeMultiplier) {
<= 1 => geometry1x,
< 2 => EdgeInsetsGeometry.lerp(
geometry1x,
geometry2x,
fontSizeMultiplier - 1,
)!,
< 3 => EdgeInsetsGeometry.lerp(
geometry2x,
geometry3x,
fontSizeMultiplier - 2,
)!,
_ => geometry3x,
};
}
}
/// The base [State] class for buttons whose style is defined by a [ButtonStyle] object.
///
/// See also:
///
/// * [ButtonStyleButton], the [StatefulWidget] subclass for which this class is the [State].
/// * [ElevatedButton], a filled button whose material elevates when pressed.
/// * [FilledButton], a filled ButtonStyleButton that doesn't elevate when pressed.
/// * [OutlinedButton], similar to [TextButton], but with an outline.
/// * [TextButton], a simple button without a shadow.
class _ButtonStyleState extends State<ButtonStyleButton>
with TickerProviderStateMixin {
AnimationController? controller;
double? elevation;
Color? backgroundColor;
WidgetStatesController? internalStatesController;
void handleStatesControllerChange() {
// Force a rebuild to resolve WidgetStateProperty properties
setState(() {});
}
WidgetStatesController get statesController =>
widget.statesController ?? internalStatesController!;
void initStatesController() {
if (widget.statesController == null) {
internalStatesController = WidgetStatesController();
}
statesController
..update(WidgetState.disabled, !widget.enabled)
..addListener(handleStatesControllerChange);
}
@override
void initState() {
super.initState();
initStatesController();
}
@override
void didUpdateWidget(ButtonStyleButton oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.statesController != oldWidget.statesController) {
oldWidget.statesController?.removeListener(handleStatesControllerChange);
if (widget.statesController != null) {
internalStatesController?.dispose();
internalStatesController = null;
}
initStatesController();
}
if (widget.enabled != oldWidget.enabled) {
statesController.update(WidgetState.disabled, !widget.enabled);
if (!widget.enabled) {
// The button may have been disabled while a press gesture is currently underway.
statesController.update(WidgetState.pressed, false);
}
}
}
@override
void dispose() {
statesController.removeListener(handleStatesControllerChange);
internalStatesController?.dispose();
controller?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
final IconThemeData iconTheme = IconTheme.of(context);
final ButtonStyle? widgetStyle = widget.style;
final ButtonStyle? themeStyle = widget.themeStyleOf(context);
final ButtonStyle defaultStyle = widget.defaultStyleOf(context);
T? effectiveValue<T>(T? Function(ButtonStyle? style) getProperty) {
final T? widgetValue = getProperty(widgetStyle);
final T? themeValue = getProperty(themeStyle);
final T? defaultValue = getProperty(defaultStyle);
return widgetValue ?? themeValue ?? defaultValue;
}
T? resolve<T>(
WidgetStateProperty<T>? Function(ButtonStyle? style) getProperty,
) {
return effectiveValue((ButtonStyle? style) {
return getProperty(style)?.resolve(statesController.value);
});
}
Color? effectiveIconColor() {
return widgetStyle?.iconColor?.resolve(statesController.value) ??
themeStyle?.iconColor?.resolve(statesController.value) ??
widgetStyle?.foregroundColor?.resolve(statesController.value) ??
themeStyle?.foregroundColor?.resolve(statesController.value) ??
defaultStyle.iconColor?.resolve(statesController.value) ??
// Fallback to foregroundColor if iconColor is null.
defaultStyle.foregroundColor?.resolve(statesController.value);
}
final double? resolvedElevation = resolve<double?>(
(ButtonStyle? style) => style?.elevation,
);
final TextStyle? resolvedTextStyle = resolve<TextStyle?>(
(ButtonStyle? style) => style?.textStyle,
);
Color? resolvedBackgroundColor = resolve<Color?>(
(ButtonStyle? style) => style?.backgroundColor,
);
final Color? resolvedForegroundColor = resolve<Color?>(
(ButtonStyle? style) => style?.foregroundColor,
);
final Color? resolvedShadowColor = resolve<Color?>(
(ButtonStyle? style) => style?.shadowColor,
);
final Color? resolvedSurfaceTintColor = resolve<Color?>(
(ButtonStyle? style) => style?.surfaceTintColor,
);
final EdgeInsetsGeometry? resolvedPadding = resolve<EdgeInsetsGeometry?>(
(ButtonStyle? style) => style?.padding,
);
final Size? resolvedMinimumSize = resolve<Size?>(
(ButtonStyle? style) => style?.minimumSize,
);
final Size? resolvedFixedSize = resolve<Size?>(
(ButtonStyle? style) => style?.fixedSize,
);
final Size? resolvedMaximumSize = resolve<Size?>(
(ButtonStyle? style) => style?.maximumSize,
);
final Color? resolvedIconColor = effectiveIconColor();
final double? resolvedIconSize = resolve<double?>(
(ButtonStyle? style) => style?.iconSize,
);
final BorderSide? resolvedSide = resolve<BorderSide?>(
(ButtonStyle? style) => style?.side,
);
final OutlinedBorder? resolvedShape = resolve<OutlinedBorder?>(
(ButtonStyle? style) => style?.shape,
);
final WidgetStateMouseCursor mouseCursor = _MouseCursor(
(Set<WidgetState> states) => effectiveValue(
(ButtonStyle? style) => style?.mouseCursor?.resolve(states),
),
);
final WidgetStateProperty<Color?> overlayColor =
WidgetStateProperty.resolveWith<Color?>(
(Set<WidgetState> states) => effectiveValue(
(ButtonStyle? style) => style?.overlayColor?.resolve(states),
),
);
final VisualDensity? resolvedVisualDensity = effectiveValue(
(ButtonStyle? style) => style?.visualDensity,
);
final MaterialTapTargetSize? resolvedTapTargetSize = effectiveValue(
(ButtonStyle? style) => style?.tapTargetSize,
);
final Duration? resolvedAnimationDuration = effectiveValue(
(ButtonStyle? style) => style?.animationDuration,
);
final bool resolvedEnableFeedback =
effectiveValue((ButtonStyle? style) => style?.enableFeedback) ?? true;
final AlignmentGeometry? resolvedAlignment = effectiveValue(
(ButtonStyle? style) => style?.alignment,
);
final Offset densityAdjustment = resolvedVisualDensity!.baseSizeAdjustment;
final InteractiveInkFeatureFactory? resolvedSplashFactory = effectiveValue(
(ButtonStyle? style) => style?.splashFactory,
);
final ButtonLayerBuilder? resolvedBackgroundBuilder = effectiveValue(
(ButtonStyle? style) => style?.backgroundBuilder,
);
final ButtonLayerBuilder? resolvedForegroundBuilder = effectiveValue(
(ButtonStyle? style) => style?.foregroundBuilder,
);
final Clip effectiveClipBehavior =
widget.clipBehavior ??
((resolvedBackgroundBuilder ?? resolvedForegroundBuilder) != null
? Clip.antiAlias
: Clip.none);
BoxConstraints effectiveConstraints = resolvedVisualDensity
.effectiveConstraints(
BoxConstraints(
minWidth: resolvedMinimumSize!.width,
minHeight: resolvedMinimumSize.height,
maxWidth: resolvedMaximumSize!.width,
maxHeight: resolvedMaximumSize.height,
),
);
if (resolvedFixedSize != null) {
final Size size = effectiveConstraints.constrain(resolvedFixedSize);
if (size.width.isFinite) {
effectiveConstraints = effectiveConstraints.copyWith(
minWidth: size.width,
maxWidth: size.width,
);
}
if (size.height.isFinite) {
effectiveConstraints = effectiveConstraints.copyWith(
minHeight: size.height,
maxHeight: size.height,
);
}
}
// Per the Material Design team: don't allow the VisualDensity
// adjustment to reduce the width of the left/right padding. If we
// did, VisualDensity.compact, the default for desktop/web, would
// reduce the horizontal padding to zero.
final double dy = densityAdjustment.dy;
final double dx = math.max(0, densityAdjustment.dx);
final EdgeInsetsGeometry padding = resolvedPadding!
.add(EdgeInsets.fromLTRB(dx, dy, dx, dy))
.clamp(EdgeInsets.zero, EdgeInsetsGeometry.infinity);
// If an opaque button's background is becoming translucent while its
// elevation is changing, change the elevation first. Material implicitly
// animates its elevation but not its color. SKIA renders non-zero
// elevations as a shadow colored fill behind the Material's background.
if (resolvedAnimationDuration! > Duration.zero &&
elevation != null &&
backgroundColor != null &&
elevation != resolvedElevation &&
backgroundColor!.value != resolvedBackgroundColor!.value &&
backgroundColor!.opacity == 1 &&
resolvedBackgroundColor.opacity < 1 &&
resolvedElevation == 0) {
if (controller?.duration != resolvedAnimationDuration) {
controller?.dispose();
controller =
AnimationController(
duration: resolvedAnimationDuration,
vsync: this,
)..addStatusListener((AnimationStatus status) {
if (status == AnimationStatus.completed) {
setState(() {}); // Rebuild with the final background color.
}
});
}
resolvedBackgroundColor =
backgroundColor; // Defer changing the background color.
controller!.value = 0;
controller!.forward();
}
elevation = resolvedElevation;
backgroundColor = resolvedBackgroundColor;
Widget result = Padding(
padding: padding,
child: Align(
alignment: resolvedAlignment!,
widthFactor: 1.0,
heightFactor: 1.0,
child: resolvedForegroundBuilder != null
? resolvedForegroundBuilder(
context,
statesController.value,
widget.child,
)
: widget.child,
),
);
if (resolvedBackgroundBuilder != null) {
result = resolvedBackgroundBuilder(
context,
statesController.value,
result,
);
}
result = AnimatedTheme(
duration: resolvedAnimationDuration,
data: theme.copyWith(
iconTheme: iconTheme.merge(
IconThemeData(color: resolvedIconColor, size: resolvedIconSize),
),
),
child: InkWell(
onTap: widget.onPressed,
onLongPress: widget.onLongPress,
onHover: widget.onHover,
mouseCursor: mouseCursor,
enableFeedback: resolvedEnableFeedback,
focusNode: widget.focusNode,
canRequestFocus: widget.enabled,
onFocusChange: widget.onFocusChange,
autofocus: widget.autofocus,
splashFactory: resolvedSplashFactory,
overlayColor: overlayColor,
highlightColor: Colors.transparent,
customBorder: resolvedShape!.copyWith(side: resolvedSide),
statesController: statesController,
child: result,
),
);
if (widget.tooltip != null) {
result = Tooltip(message: widget.tooltip, child: result);
}
final Size minSize;
switch (resolvedTapTargetSize!) {
case MaterialTapTargetSize.padded:
minSize = Size(
kMinInteractiveDimension + densityAdjustment.dx,
kMinInteractiveDimension + densityAdjustment.dy,
);
assert(minSize.width >= 0.0);
assert(minSize.height >= 0.0);
case MaterialTapTargetSize.shrinkWrap:
minSize = Size.zero;
}
return Semantics(
container: true,
button: widget.isSemanticButton,
enabled: widget.enabled,
child: _InputPadding(
minSize: minSize,
child: ConstrainedBox(
constraints: effectiveConstraints,
child: Material(
elevation: resolvedElevation!,
textStyle: resolvedTextStyle?.copyWith(
color: resolvedForegroundColor,
),
shape: resolvedShape.copyWith(side: resolvedSide),
color: resolvedBackgroundColor,
shadowColor: resolvedShadowColor,
surfaceTintColor: resolvedSurfaceTintColor,
type: resolvedBackgroundColor == null
? MaterialType.transparency
: MaterialType.button,
animationDuration: resolvedAnimationDuration,
clipBehavior: effectiveClipBehavior,
borderOnForeground: false,
child: result,
),
),
),
);
}
}
class _MouseCursor extends WidgetStateMouseCursor {
const _MouseCursor(this.resolveCallback);
final WidgetPropertyResolver<MouseCursor?> resolveCallback;
@override
MouseCursor resolve(Set<WidgetState> states) => resolveCallback(states)!;
@override
String get debugDescription => 'ButtonStyleButton_MouseCursor';
}
/// A widget to pad the area around a [ButtonStyleButton]'s inner [Material].
///
/// Redirect taps that occur in the padded area around the child to the center
/// of the child. This increases the size of the button and the button's
/// "tap target", but not its material or its ink splashes.
class _InputPadding extends SingleChildRenderObjectWidget {
const _InputPadding({super.child, required this.minSize});
final Size minSize;
@override
RenderObject createRenderObject(BuildContext context) {
return _RenderInputPadding(minSize);
}
@override
void updateRenderObject(
BuildContext context,
covariant _RenderInputPadding renderObject,
) {
renderObject.minSize = minSize;
}
}
class _RenderInputPadding extends RenderShiftedBox {
_RenderInputPadding(this._minSize, [RenderBox? child]) : super(child);
Size get minSize => _minSize;
Size _minSize;
set minSize(Size value) {
if (_minSize == value) {
return;
}
_minSize = value;
markNeedsLayout();
}
@override
double computeMinIntrinsicWidth(double height) {
if (child != null) {
return math.max(child!.getMinIntrinsicWidth(height), minSize.width);
}
return 0.0;
}
@override
double computeMinIntrinsicHeight(double width) {
if (child != null) {
return math.max(child!.getMinIntrinsicHeight(width), minSize.height);
}
return 0.0;
}
@override
double computeMaxIntrinsicWidth(double height) {
if (child != null) {
return math.max(child!.getMaxIntrinsicWidth(height), minSize.width);
}
return 0.0;
}
@override
double computeMaxIntrinsicHeight(double width) {
if (child != null) {
return math.max(child!.getMaxIntrinsicHeight(width), minSize.height);
}
return 0.0;
}
Size _computeSize({
required BoxConstraints constraints,
required ChildLayouter layoutChild,
}) {
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));
}
return Size.zero;
}
@override
Size computeDryLayout(BoxConstraints constraints) {
return _computeSize(
constraints: constraints,
layoutChild: ChildLayoutHelper.dryLayoutChild,
);
}
@override
double? computeDryBaseline(
covariant BoxConstraints constraints,
TextBaseline baseline,
) {
final RenderBox? child = this.child;
if (child == null) {
return null;
}
final double? result = child.getDryBaseline(constraints, baseline);
if (result == null) {
return null;
}
final Size childSize = child.getDryLayout(constraints);
return result +
Alignment.center
.alongOffset(getDryLayout(constraints) - childSize as Offset)
.dy;
}
@override
void performLayout() {
size = _computeSize(
constraints: constraints,
layoutChild: ChildLayoutHelper.layoutChild,
);
if (child != null) {
final BoxParentData childParentData = child!.parentData! as BoxParentData;
childParentData.offset = Alignment.center.alongOffset(
size - child!.size as Offset,
);
}
}
@override
bool hitTest(BoxHitTestResult result, {required Offset position}) {
if (super.hitTest(result, position: position)) {
return true;
}
final Offset center = child!.size.center(Offset.zero);
return result.addWithRawTransform(
transform: MatrixUtils.forceToPoint(center),
position: center,
hitTest: (BoxHitTestResult result, Offset position) {
assert(position == center);
return child!.hitTest(result, position: center);
},
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,676 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// 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 'elevated_button.dart';
/// @docImport 'filled_button.dart';
/// @docImport 'material.dart';
/// @docImport 'outlined_button.dart';
library;
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;
/// A Material Design "Text Button".
///
/// Use text buttons on toolbars, in dialogs, or inline with other
/// content but offset from that content with padding so that the
/// button's presence is obvious. Text buttons do not have visible
/// borders and must therefore rely on their position relative to
/// other content for context. In dialogs and cards, they should be
/// grouped together in one of the bottom corners. Avoid using text
/// buttons where they would blend in with other content, for example
/// in the middle of lists.
///
/// A text button is a label [child] displayed on a (zero elevation)
/// [Material] widget. The label's [Text] and [Icon] widgets are
/// displayed in the [style]'s [ButtonStyle.foregroundColor]. The
/// button reacts to touches by filling with the [style]'s
/// [ButtonStyle.backgroundColor].
///
/// The text button's default style is defined by [defaultStyleOf].
/// The style of this text button can be overridden with its [style]
/// parameter. The style of all text buttons in a subtree can be
/// overridden with the [TextButtonTheme] and the style of all of the
/// text buttons in an app can be overridden with the [Theme]'s
/// [ThemeData.textButtonTheme] property.
///
/// The static [styleFrom] method is a convenient way to create a
/// text button [ButtonStyle] from simple values.
///
/// If the [onPressed] and [onLongPress] callbacks are null, then this
/// button will be disabled, it will not react to touch.
///
/// {@tool dartpad}
/// This sample shows various ways to configure TextButtons, from the
/// simplest default appearance to versions that don't resemble
/// Material Design at all.
///
/// ** See code in examples/api/lib/material/text_button/text_button.0.dart **
/// {@end-tool}
///
/// {@tool dartpad}
/// This sample demonstrates using the [statesController] parameter to create a button
/// that adds support for [WidgetState.selected].
///
/// ** See code in examples/api/lib/material/text_button/text_button.1.dart **
/// {@end-tool}
///
/// See also:
///
/// * [ElevatedButton], a filled button whose material elevates when pressed.
/// * [FilledButton], a filled button that doesn't elevate when pressed.
/// * [FilledButton.tonal], a filled button variant that uses a secondary fill color.
/// * [OutlinedButton], a button with an outlined border and no fill color.
/// * <https://material.io/design/components/buttons.html>
/// * <https://m3.material.io/components/buttons>
class TextButton extends ButtonStyleButton {
/// Create a [TextButton].
const TextButton({
super.key,
required super.onPressed,
super.onLongPress,
super.onHover,
super.onFocusChange,
super.style,
super.focusNode,
super.autofocus = false,
super.clipBehavior,
super.statesController,
super.isSemanticButton,
required Widget super.child,
});
/// Create a text button from a pair of widgets that serve as the button's
/// [icon] and [label].
///
/// 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.
///
/// {@macro flutter.material.ButtonStyleButton.iconAlignment}
///
factory TextButton.icon({
Key? key,
required VoidCallback? onPressed,
VoidCallback? onLongPress,
ValueChanged<bool>? onHover,
ValueChanged<bool>? onFocusChange,
ButtonStyle? style,
FocusNode? focusNode,
bool? autofocus,
Clip? clipBehavior,
WidgetStatesController? 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,
);
}
/// A static convenience method that constructs a text button
/// [ButtonStyle] given simple values.
///
/// The [foregroundColor] and [disabledForegroundColor] colors are used
/// to create a [WidgetStateProperty] [ButtonStyle.foregroundColor], and
/// a derived [ButtonStyle.overlayColor] if [overlayColor] isn't specified.
///
/// The [backgroundColor] and [disabledBackgroundColor] colors are
/// used to create a [WidgetStateProperty] [ButtonStyle.backgroundColor].
///
/// Similarly, the [enabledMouseCursor] and [disabledMouseCursor]
/// parameters are used to construct [ButtonStyle.mouseCursor].
///
/// The [iconColor], [disabledIconColor] are used to construct
/// [ButtonStyle.iconColor] and [iconSize] is used to construct
/// [ButtonStyle.iconSize].
///
/// If [iconColor] is null, the button icon will use [foregroundColor]. If [foregroundColor] is also
/// null, the button icon will use the default icon color.
///
/// If [overlayColor] is specified and its value is [Colors.transparent]
/// then the pressed/focused/hovered highlights are effectively defeated.
/// Otherwise a [WidgetStateProperty] with the same opacities as the
/// default is created.
///
/// All of the other parameters are either used directly or used to
/// create a [WidgetStateProperty] with a single value for all
/// states.
///
/// All parameters default to null. By default this method returns
/// a [ButtonStyle] that doesn't override anything.
///
/// For example, to override the default text and icon colors for a
/// [TextButton], as well as its overlay color, with all of the
/// standard opacity adjustments for the pressed, focused, and
/// hovered states, one could write:
///
/// ```dart
/// TextButton(
/// style: TextButton.styleFrom(foregroundColor: Colors.green),
/// child: const Text('Give Kate a mix tape'),
/// onPressed: () {
/// // ...
/// },
/// ),
/// ```
static ButtonStyle styleFrom({
Color? foregroundColor,
Color? backgroundColor,
Color? disabledForegroundColor,
Color? disabledBackgroundColor,
Color? shadowColor,
Color? surfaceTintColor,
Color? iconColor,
double? iconSize,
IconAlignment? iconAlignment,
Color? disabledIconColor,
Color? overlayColor,
double? elevation,
TextStyle? textStyle,
EdgeInsetsGeometry? padding,
Size? minimumSize,
Size? fixedSize,
Size? maximumSize,
BorderSide? side,
OutlinedBorder? shape,
MouseCursor? enabledMouseCursor,
MouseCursor? disabledMouseCursor,
VisualDensity? visualDensity,
MaterialTapTargetSize? tapTargetSize,
Duration? animationDuration,
bool? enableFeedback,
AlignmentGeometry? alignment,
InteractiveInkFeatureFactory? splashFactory,
ButtonLayerBuilder? backgroundBuilder,
ButtonLayerBuilder? foregroundBuilder,
}) {
final WidgetStateProperty<Color?>? backgroundColorProp = switch ((
backgroundColor,
disabledBackgroundColor,
)) {
(_?, null) => WidgetStatePropertyAll<Color?>(backgroundColor),
(_, _) => ButtonStyleButton.defaultColor(
backgroundColor,
disabledBackgroundColor,
),
};
final WidgetStateProperty<Color?>? iconColorProp = switch ((
iconColor,
disabledIconColor,
)) {
(_?, null) => WidgetStatePropertyAll<Color?>(iconColor),
(_, _) => ButtonStyleButton.defaultColor(iconColor, disabledIconColor),
};
final WidgetStateProperty<Color?>? overlayColorProp = switch ((
foregroundColor,
overlayColor,
)) {
(null, null) => null,
(_, Color(a: 0.0)) => WidgetStatePropertyAll<Color?>(overlayColor),
(_, final Color color) || (final Color color, _) =>
WidgetStateProperty<Color?>.fromMap(<WidgetState, Color?>{
WidgetState.pressed: color.withValues(alpha: 0.1),
WidgetState.hovered: color.withValues(alpha: 0.08),
WidgetState.focused: color.withValues(alpha: 0.1),
}),
};
return ButtonStyle(
textStyle: ButtonStyleButton.allOrNull<TextStyle>(textStyle),
foregroundColor: ButtonStyleButton.defaultColor(
foregroundColor,
disabledForegroundColor,
),
backgroundColor: backgroundColorProp,
overlayColor: overlayColorProp,
shadowColor: ButtonStyleButton.allOrNull<Color>(shadowColor),
surfaceTintColor: ButtonStyleButton.allOrNull<Color>(surfaceTintColor),
iconColor: iconColorProp,
iconSize: ButtonStyleButton.allOrNull<double>(iconSize),
iconAlignment: iconAlignment,
elevation: ButtonStyleButton.allOrNull<double>(elevation),
padding: ButtonStyleButton.allOrNull<EdgeInsetsGeometry>(padding),
minimumSize: ButtonStyleButton.allOrNull<Size>(minimumSize),
fixedSize: ButtonStyleButton.allOrNull<Size>(fixedSize),
maximumSize: ButtonStyleButton.allOrNull<Size>(maximumSize),
side: ButtonStyleButton.allOrNull<BorderSide>(side),
shape: ButtonStyleButton.allOrNull<OutlinedBorder>(shape),
mouseCursor: WidgetStateProperty<MouseCursor?>.fromMap(
<WidgetStatesConstraint, MouseCursor?>{
WidgetState.disabled: disabledMouseCursor,
WidgetState.any: enabledMouseCursor,
},
),
visualDensity: visualDensity,
tapTargetSize: tapTargetSize,
animationDuration: animationDuration,
enableFeedback: enableFeedback,
alignment: alignment,
splashFactory: splashFactory,
backgroundBuilder: backgroundBuilder,
foregroundBuilder: foregroundBuilder,
);
}
/// Defines the button's default appearance.
///
/// {@template flutter.material.text_button.default_style_of}
/// The button [child]'s [Text] and [Icon] widgets are rendered with
/// the [ButtonStyle]'s foreground color. The button's [InkWell] adds
/// the style's overlay color when the button is focused, hovered
/// or pressed. The button's background color becomes its [Material]
/// color and is transparent by default.
///
/// All of the [ButtonStyle]'s defaults appear below.
///
/// In this list "Theme.foo" is shorthand for
/// `Theme.of(context).foo`. Color scheme values like
/// "onSurface(0.38)" are shorthand for
/// `onSurface.withValues(alpha: 0.38)`. [WidgetStateProperty] valued
/// properties that are not followed by a sublist have the same
/// value for all states, otherwise the values are as specified for
/// each state and "others" means all other states.
///
/// The "default font size" below refers to the font size specified in the
/// [defaultStyleOf] method (or 14.0 if unspecified), scaled by the
/// `MediaQuery.textScalerOf(context).scale` method. And the names of the
/// EdgeInsets constructors and `EdgeInsetsGeometry.lerp` have been abbreviated
/// for readability.
///
/// The color of the [ButtonStyle.textStyle] is not used, the
/// [ButtonStyle.foregroundColor] color is used instead.
/// {@endtemplate}
///
/// ## Material 2 defaults
///
/// * `textStyle` - Theme.textTheme.button
/// * `backgroundColor` - transparent
/// * `foregroundColor`
/// * disabled - Theme.colorScheme.onSurface(0.38)
/// * others - Theme.colorScheme.primary
/// * `overlayColor`
/// * hovered - Theme.colorScheme.primary(0.08)
/// * focused or pressed - Theme.colorScheme.primary(0.12)
/// * `shadowColor` - Theme.shadowColor
/// * `elevation` - 0
/// * `padding`
/// * `default font size <= 14` - (horizontal(12), vertical(8))
/// * `14 < default font size <= 28` - lerp(all(8), horizontal(8))
/// * `28 < default font size <= 36` - lerp(horizontal(8), horizontal(4))
/// * `36 < default font size` - horizontal(4)
/// * `minimumSize` - Size(64, 36)
/// * `fixedSize` - null
/// * `maximumSize` - Size.infinite
/// * `side` - null
/// * `shape` - RoundedRectangleBorder(borderRadius: BorderRadius.circular(4))
/// * `mouseCursor`
/// * disabled - SystemMouseCursors.basic
/// * others - SystemMouseCursors.click
/// * `visualDensity` - theme.visualDensity
/// * `tapTargetSize` - theme.materialTapTargetSize
/// * `animationDuration` - kThemeChangeDuration
/// * `enableFeedback` - true
/// * `alignment` - Alignment.center
/// * `splashFactory` - InkRipple.splashFactory
///
/// The default padding values for the [TextButton.icon] factory are slightly different:
///
/// * `padding`
/// * `default font size <= 14` - all(8)
/// * `14 < default font size <= 28 `- lerp(all(8), horizontal(4))
/// * `28 < default font size` - horizontal(4)
///
/// The default value for `side`, which defines the appearance of the button's
/// outline, is null. That means that the outline is defined by the button
/// shape's [OutlinedBorder.side]. Typically the default value of an
/// [OutlinedBorder]'s side is [BorderSide.none], so an outline is not drawn.
///
/// ## Material 3 defaults
///
/// If [ThemeData.useMaterial3] is set to true the following defaults will
/// be used:
///
/// {@template flutter.material.text_button.material3_defaults}
/// * `textStyle` - Theme.textTheme.labelLarge
/// * `backgroundColor` - transparent
/// * `foregroundColor`
/// * disabled - Theme.colorScheme.onSurface(0.38)
/// * others - Theme.colorScheme.primary
/// * `overlayColor`
/// * hovered - Theme.colorScheme.primary(0.08)
/// * focused or pressed - Theme.colorScheme.primary(0.1)
/// * others - null
/// * `shadowColor` - Colors.transparent,
/// * `surfaceTintColor` - null
/// * `elevation` - 0
/// * `padding`
/// * `default font size <= 14` - lerp(horizontal(12), horizontal(4))
/// * `14 < default font size <= 28` - lerp(all(8), horizontal(8))
/// * `28 < default font size <= 36` - lerp(horizontal(8), horizontal(4))
/// * `36 < default font size` - horizontal(4)
/// * `minimumSize` - Size(64, 40)
/// * `fixedSize` - null
/// * `maximumSize` - Size.infinite
/// * `side` - null
/// * `shape` - StadiumBorder()
/// * `mouseCursor`
/// * disabled - SystemMouseCursors.basic
/// * others - SystemMouseCursors.click
/// * `visualDensity` - theme.visualDensity
/// * `tapTargetSize` - theme.materialTapTargetSize
/// * `animationDuration` - kThemeChangeDuration
/// * `enableFeedback` - true
/// * `alignment` - Alignment.center
/// * `splashFactory` - Theme.splashFactory
///
/// For the [TextButton.icon] factory, the end (generally the right) value of
/// `padding` is increased from 12 to 16.
/// {@endtemplate}
@override
ButtonStyle defaultStyleOf(BuildContext context) {
final ThemeData theme = Theme.of(context);
final ColorScheme colorScheme = theme.colorScheme;
return Theme.of(context).useMaterial3
? _TextButtonDefaultsM3(context)
: styleFrom(
foregroundColor: colorScheme.primary,
disabledForegroundColor: colorScheme.onSurface.withValues(
alpha: 0.38,
),
backgroundColor: Colors.transparent,
disabledBackgroundColor: Colors.transparent,
shadowColor: theme.shadowColor,
elevation: 0,
textStyle: theme.textTheme.labelLarge,
padding: _scaledPadding(context),
minimumSize: const Size(64, 36),
maximumSize: Size.infinite,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(4)),
),
enabledMouseCursor: SystemMouseCursors.click,
disabledMouseCursor: SystemMouseCursors.basic,
visualDensity: theme.visualDensity,
tapTargetSize: theme.materialTapTargetSize,
animationDuration: kThemeChangeDuration,
enableFeedback: true,
alignment: Alignment.center,
splashFactory: InkRipple.splashFactory,
);
}
/// Returns the [TextButtonThemeData.style] of the closest
/// [TextButtonTheme] ancestor.
@override
ButtonStyle? themeStyleOf(BuildContext context) {
return TextButtonTheme.of(context).style;
}
}
EdgeInsetsGeometry _scaledPadding(BuildContext context) {
final ThemeData theme = Theme.of(context);
final double defaultFontSize = theme.textTheme.labelLarge?.fontSize ?? 14.0;
final double effectiveTextScale =
MediaQuery.textScalerOf(context).scale(defaultFontSize) / 14.0;
return ButtonStyleButton.scaledPadding(
theme.useMaterial3
? const EdgeInsets.symmetric(horizontal: 12, vertical: 8)
: const EdgeInsets.all(8),
const EdgeInsets.symmetric(horizontal: 8),
const EdgeInsets.symmetric(horizontal: 4),
effectiveTextScale,
);
}
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 <WidgetState>{})?.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<EdgeInsetsGeometry>(scaledPadding),
);
}
}
class _TextButtonWithIconChild extends StatelessWidget {
const _TextButtonWithIconChild({
required this.label,
required this.icon,
required this.buttonStyle,
required this.iconAlignment,
});
final Widget label;
final Widget icon;
final ButtonStyle? buttonStyle;
final IconAlignment? iconAlignment;
@override
Widget build(BuildContext context) {
final double defaultFontSize =
buttonStyle?.textStyle?.resolve(const <WidgetState>{})?.fontSize ??
14.0;
final double scale =
clampDouble(
MediaQuery.textScalerOf(context).scale(defaultFontSize) / 14.0,
1.0,
2.0,
) -
1.0;
final TextButtonThemeData textButtonTheme = TextButtonTheme.of(context);
final IconAlignment effectiveIconAlignment =
iconAlignment ??
textButtonTheme.style?.iconAlignment ??
buttonStyle?.iconAlignment ??
IconAlignment.start;
return Row(
mainAxisSize: MainAxisSize.min,
spacing: lerpDouble(8, 4, scale)!,
children: effectiveIconAlignment == IconAlignment.start
? <Widget>[icon, Flexible(child: label)]
: <Widget>[Flexible(child: label), icon],
);
}
}
// BEGIN GENERATED TOKEN PROPERTIES - TextButton
// Do not edit by hand. The code between the "BEGIN GENERATED" and
// "END GENERATED" comments are generated from data in the Material
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
// dart format off
class _TextButtonDefaultsM3 extends ButtonStyle {
_TextButtonDefaultsM3(this.context)
: super(
animationDuration: kThemeChangeDuration,
enableFeedback: true,
alignment: Alignment.center,
);
final BuildContext context;
late final ColorScheme _colors = Theme.of(context).colorScheme;
@override
WidgetStateProperty<TextStyle?> get textStyle =>
WidgetStatePropertyAll<TextStyle?>(Theme.of(context).textTheme.labelLarge);
@override
WidgetStateProperty<Color?>? get backgroundColor =>
const WidgetStatePropertyAll<Color>(Colors.transparent);
@override
WidgetStateProperty<Color?>? get foregroundColor =>
WidgetStateProperty.resolveWith((Set<WidgetState> states) {
if (states.contains(WidgetState.disabled)) {
return _colors.onSurface.withValues(alpha: 0.38);
}
return _colors.primary;
});
@override
WidgetStateProperty<Color?>? get overlayColor =>
WidgetStateProperty.resolveWith((Set<WidgetState> 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<Color>? get shadowColor =>
const WidgetStatePropertyAll<Color>(Colors.transparent);
@override
WidgetStateProperty<Color>? get surfaceTintColor =>
const WidgetStatePropertyAll<Color>(Colors.transparent);
@override
WidgetStateProperty<double>? get elevation =>
const WidgetStatePropertyAll<double>(0.0);
@override
WidgetStateProperty<EdgeInsetsGeometry>? get padding =>
WidgetStatePropertyAll<EdgeInsetsGeometry>(_scaledPadding(context));
@override
WidgetStateProperty<Size>? get minimumSize =>
const WidgetStatePropertyAll<Size>(Size(64.0, 40.0));
// No default fixedSize
@override
WidgetStateProperty<double>? get iconSize =>
const WidgetStatePropertyAll<double>(18.0);
@override
WidgetStateProperty<Color>? get iconColor {
return WidgetStateProperty.resolveWith((Set<WidgetState> states) {
if (states.contains(WidgetState.disabled)) {
return _colors.onSurface.withValues(alpha: 0.38);
}
if (states.contains(WidgetState.pressed)) {
return _colors.primary;
}
if (states.contains(WidgetState.hovered)) {
return _colors.primary;
}
if (states.contains(WidgetState.focused)) {
return _colors.primary;
}
return _colors.primary;
});
}
@override
WidgetStateProperty<Size>? get maximumSize =>
const WidgetStatePropertyAll<Size>(Size.infinite);
// No default side
@override
WidgetStateProperty<OutlinedBorder>? get shape =>
const WidgetStatePropertyAll<OutlinedBorder>(StadiumBorder());
@override
WidgetStateProperty<MouseCursor?>? get mouseCursor =>
WidgetStateProperty.resolveWith((Set<WidgetState> states) {
if (states.contains(WidgetState.disabled)) {
return SystemMouseCursors.basic;
}
return SystemMouseCursors.click;
});
@override
VisualDensity? get visualDensity => Theme.of(context).visualDensity;
@override
MaterialTapTargetSize? get tapTargetSize => Theme.of(context).materialTapTargetSize;
@override
InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory;
}
// dart format on
// END GENERATED TOKEN PROPERTIES - TextButton

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,461 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// 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;
import 'package:flutter/material.dart' hide Scrollable, ScrollableState;
import 'package:flutter/rendering.dart';
class _ForceImplicitScrollPhysics extends ScrollPhysics {
const _ForceImplicitScrollPhysics({
required this.allowImplicitScrolling,
super.parent,
});
@override
_ForceImplicitScrollPhysics applyTo(ScrollPhysics? ancestor) {
return _ForceImplicitScrollPhysics(
allowImplicitScrolling: allowImplicitScrolling,
parent: buildParent(ancestor),
);
}
@override
final bool allowImplicitScrolling;
}
const PageScrollPhysics _kPagePhysics = PageScrollPhysics();
/// A scrollable list that works page by page.
///
/// Each child of a page view is forced to be the same size as the viewport.
///
/// You can use a [PageController] to control which page is visible in the view.
/// In addition to being able to control the pixel offset of the content inside
/// the [CustomPageView], a [PageController] also lets you control the offset in terms
/// of pages, which are increments of the viewport size.
///
/// The [PageController] can also be used to control the
/// [PageController.initialPage], which determines which page is shown when the
/// [CustomPageView] is first constructed, and the [PageController.viewportFraction],
/// which determines the size of the pages as a fraction of the viewport size.
///
/// {@youtube 560 315 https://www.youtube.com/watch?v=J1gE9xvph-A}
///
/// {@tool dartpad}
/// Here is an example of [CustomPageView]. It creates a centered [Text] in each of the three pages
/// which scroll horizontally.
///
/// ** See code in examples/api/lib/widgets/page_view/page_view.0.dart **
/// {@end-tool}
///
/// ## Persisting the scroll position during a session
///
/// Scroll views attempt to persist their scroll position using [PageStorage].
/// For a [CustomPageView], this can be disabled by setting [PageController.keepPage]
/// to false on the [controller]. If it is enabled, using a [PageStorageKey] for
/// the [key] of this widget is recommended to help disambiguate different
/// scroll views from each other.
///
/// See also:
///
/// * [PageController], which controls which page is visible in the view.
/// * [SingleChildScrollView], when you need to make a single child scrollable.
/// * [ListView], for a scrollable list of boxes.
/// * [GridView], for a scrollable grid of boxes.
/// * [ScrollNotification] and [NotificationListener], which can be used to watch
/// the scroll position without using a [ScrollController].
class CustomPageView extends StatefulWidget {
/// Creates a scrollable list that works page by page from an explicit [List]
/// of widgets.
///
/// This constructor is appropriate for page views with a small number of
/// children because constructing the [List] requires doing work for every
/// child that could possibly be displayed in the page view, instead of just
/// those children that are actually visible.
///
/// Like other widgets in the framework, this widget expects that
/// the [children] list will not be mutated after it has been passed in here.
/// See the documentation at [SliverChildListDelegate.children] for more details.
///
/// {@template flutter.widgets.PageView.allowImplicitScrolling}
/// If [allowImplicitScrolling] is true, the [CustomPageView] will participate in
/// accessibility scrolling more like a [ListView], where implicit scroll
/// actions will move to the next page rather than into the contents of the
/// [CustomPageView].
/// {@endtemplate}
CustomPageView({
super.key,
this.scrollDirection = Axis.horizontal,
this.reverse = false,
this.controller,
this.physics,
this.pageSnapping = true,
this.onPageChanged,
List<Widget> children = const <Widget>[],
this.dragStartBehavior = DragStartBehavior.start,
this.allowImplicitScrolling = false,
this.restorationId,
this.clipBehavior = Clip.hardEdge,
this.hitTestBehavior = HitTestBehavior.opaque,
this.scrollBehavior,
this.padEnds = true,
this.header,
this.bgColor = Colors.transparent,
}) : childrenDelegate = SliverChildListDelegate(children);
final Widget? header;
final Color bgColor;
/// Creates a scrollable list that works page by page using widgets that are
/// created on demand.
///
/// This constructor is appropriate for page views with a large (or infinite)
/// number of children because the builder is called only for those children
/// that are actually visible.
///
/// Providing a non-null [itemCount] lets the [CustomPageView] compute the maximum
/// scroll extent.
///
/// [itemBuilder] will be called only with indices greater than or equal to
/// zero and less than [itemCount].
///
/// {@macro flutter.widgets.ListView.builder.itemBuilder}
///
/// {@template flutter.widgets.PageView.findChildIndexCallback}
/// The [findChildIndexCallback] corresponds to the
/// [SliverChildBuilderDelegate.findChildIndexCallback] property. If null,
/// a child widget may not map to its existing [RenderObject] when the order
/// of children returned from the children builder changes.
/// This may result in state-loss. This callback needs to be implemented if
/// the order of the children may change at a later time.
/// {@endtemplate}
///
/// {@macro flutter.widgets.PageView.allowImplicitScrolling}
CustomPageView.builder({
super.key,
this.scrollDirection = Axis.horizontal,
this.reverse = false,
this.controller,
this.physics,
this.pageSnapping = true,
this.onPageChanged,
required NullableIndexedWidgetBuilder itemBuilder,
ChildIndexGetter? findChildIndexCallback,
int? itemCount,
this.dragStartBehavior = DragStartBehavior.start,
this.allowImplicitScrolling = false,
this.restorationId,
this.clipBehavior = Clip.hardEdge,
this.hitTestBehavior = HitTestBehavior.opaque,
this.scrollBehavior,
this.padEnds = true,
this.header,
this.bgColor = Colors.transparent,
}) : childrenDelegate = SliverChildBuilderDelegate(
itemBuilder,
findChildIndexCallback: findChildIndexCallback,
childCount: itemCount,
);
/// Creates a scrollable list that works page by page with a custom child
/// model.
///
/// {@tool dartpad}
/// This example shows a [CustomPageView] that uses a custom [SliverChildBuilderDelegate] to support child
/// reordering.
///
/// ** See code in examples/api/lib/widgets/page_view/page_view.1.dart **
/// {@end-tool}
///
/// {@macro flutter.widgets.PageView.allowImplicitScrolling}
const CustomPageView.custom({
super.key,
this.scrollDirection = Axis.horizontal,
this.reverse = false,
this.controller,
this.physics,
this.pageSnapping = true,
this.onPageChanged,
required this.childrenDelegate,
this.dragStartBehavior = DragStartBehavior.start,
this.allowImplicitScrolling = false,
this.restorationId,
this.clipBehavior = Clip.hardEdge,
this.hitTestBehavior = HitTestBehavior.opaque,
this.scrollBehavior,
this.padEnds = true,
this.header,
this.bgColor = Colors.transparent,
});
/// Controls whether the widget's pages will respond to
/// [RenderObject.showOnScreen], which will allow for implicit accessibility
/// scrolling.
///
/// With this flag set to false, when accessibility focus reaches the end of
/// the current page and the user attempts to move it to the next element, the
/// focus will traverse to the next widget outside of the page view.
///
/// With this flag set to true, when accessibility focus reaches the end of
/// the current page and user attempts to move it to the next element, focus
/// will traverse to the next page in the page view.
final bool allowImplicitScrolling;
/// {@macro flutter.widgets.scrollable.restorationId}
final String? restorationId;
/// The [Axis] along which the scroll view's offset increases with each page.
///
/// For the direction in which active scrolling may be occurring, see
/// [ScrollDirection].
///
/// Defaults to [Axis.horizontal].
final Axis scrollDirection;
/// Whether the page view scrolls in the reading direction.
///
/// For example, if the reading direction is left-to-right and
/// [scrollDirection] is [Axis.horizontal], then the page view scrolls from
/// left to right when [reverse] is false and from right to left when
/// [reverse] is true.
///
/// Similarly, if [scrollDirection] is [Axis.vertical], then the page view
/// scrolls from top to bottom when [reverse] is false and from bottom to top
/// when [reverse] is true.
///
/// Defaults to false.
final bool reverse;
/// An object that can be used to control the position to which this page
/// view is scrolled.
final PageController? controller;
/// How the page view should respond to user input.
///
/// For example, determines how the page view continues to animate after the
/// user stops dragging the page view.
///
/// The physics are modified to snap to page boundaries using
/// [PageScrollPhysics] prior to being used.
///
/// If an explicit [ScrollBehavior] is provided to [scrollBehavior], the
/// [ScrollPhysics] provided by that behavior will take precedence after
/// [physics].
///
/// Defaults to matching platform conventions.
final ScrollPhysics? physics;
/// Set to false to disable page snapping, useful for custom scroll behavior.
///
/// If the [padEnds] is false and [PageController.viewportFraction] < 1.0,
/// the page will snap to the beginning of the viewport; otherwise, the page
/// will snap to the center of the viewport.
final bool pageSnapping;
/// Called whenever the page in the center of the viewport changes.
final ValueChanged<int>? onPageChanged;
/// A delegate that provides the children for the [CustomPageView].
///
/// The [PageView.custom] constructor lets you specify this delegate
/// explicitly. The [CustomPageView] and [PageView.builder] constructors create a
/// [childrenDelegate] that wraps the given [List] and [IndexedWidgetBuilder],
/// respectively.
final SliverChildDelegate childrenDelegate;
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
final DragStartBehavior dragStartBehavior;
/// {@macro flutter.material.Material.clipBehavior}
///
/// Defaults to [Clip.hardEdge].
final Clip clipBehavior;
/// {@macro flutter.widgets.scrollable.hitTestBehavior}
///
/// Defaults to [HitTestBehavior.opaque].
final HitTestBehavior hitTestBehavior;
/// {@macro flutter.widgets.scrollable.scrollBehavior}
///
/// The [ScrollBehavior] of the inherited [ScrollConfiguration] will be
/// modified by default to not apply a [Scrollbar].
final ScrollBehavior? scrollBehavior;
/// Whether to add padding to both ends of the list.
///
/// If this is set to true and [PageController.viewportFraction] < 1.0, padding will be added
/// such that the first and last child slivers will be in the center of
/// the viewport when scrolled all the way to the start or end, respectively.
///
/// If [PageController.viewportFraction] >= 1.0, this property has no effect.
///
/// This property defaults to true.
final bool padEnds;
@override
State<CustomPageView> createState() => _CustomPageViewState();
}
class _CustomPageViewState extends State<CustomPageView> {
int _lastReportedPage = 0;
late PageController _controller;
@override
void initState() {
super.initState();
_initController();
_lastReportedPage = _controller.initialPage;
}
@override
void dispose() {
if (widget.controller == null) {
_controller.dispose();
}
super.dispose();
}
void _initController() {
_controller = widget.controller ?? PageController();
}
@override
void didUpdateWidget(CustomPageView oldWidget) {
if (oldWidget.controller != widget.controller) {
if (oldWidget.controller == null) {
_controller.dispose();
}
_initController();
}
super.didUpdateWidget(oldWidget);
}
AxisDirection _getDirection(BuildContext context) {
switch (widget.scrollDirection) {
case Axis.horizontal:
assert(debugCheckHasDirectionality(context));
final TextDirection textDirection = Directionality.of(context);
final AxisDirection axisDirection = textDirectionToAxisDirection(
textDirection,
);
return widget.reverse
? flipAxisDirection(axisDirection)
: axisDirection;
case Axis.vertical:
return widget.reverse ? AxisDirection.up : AxisDirection.down;
}
}
@override
Widget build(BuildContext context) {
final AxisDirection axisDirection = _getDirection(context);
final ScrollPhysics physics =
_ForceImplicitScrollPhysics(
allowImplicitScrolling: widget.allowImplicitScrolling,
).applyTo(
widget.pageSnapping
? _kPagePhysics.applyTo(
widget.physics ??
widget.scrollBehavior?.getScrollPhysics(context),
)
: widget.physics ??
widget.scrollBehavior?.getScrollPhysics(context),
);
return NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification notification) {
if (notification.depth == 0 &&
widget.onPageChanged != null &&
notification is ScrollUpdateNotification) {
final PageMetrics metrics = notification.metrics as PageMetrics;
final int currentPage = metrics.page!.round();
if (currentPage != _lastReportedPage) {
_lastReportedPage = currentPage;
widget.onPageChanged!(currentPage);
}
}
return false;
},
child: CustomScrollable(
header: widget.header,
bgColor: widget.bgColor,
dragStartBehavior: widget.dragStartBehavior,
axisDirection: axisDirection,
controller: _controller,
physics: physics,
restorationId: widget.restorationId,
hitTestBehavior: widget.hitTestBehavior,
scrollBehavior:
widget.scrollBehavior ??
ScrollConfiguration.of(context).copyWith(scrollbars: false),
viewportBuilder: (BuildContext context, ViewportOffset position) {
return Viewport(
// TODO(dnfield): we should provide a way to set cacheExtent
// independent of implicit scrolling:
// https://github.com/flutter/flutter/issues/45632
cacheExtent: widget.allowImplicitScrolling ? 1.0 : 0.0,
cacheExtentStyle: CacheExtentStyle.viewport,
axisDirection: axisDirection,
offset: position,
clipBehavior: widget.clipBehavior,
slivers: <Widget>[
SliverFillViewport(
viewportFraction: _controller.viewportFraction,
delegate: widget.childrenDelegate,
padEnds: widget.padEnds,
),
],
);
},
),
);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder description) {
super.debugFillProperties(description);
description
..add(EnumProperty<Axis>('scrollDirection', widget.scrollDirection))
..add(FlagProperty('reverse', value: widget.reverse, ifTrue: 'reversed'))
..add(
DiagnosticsProperty<PageController>(
'controller',
_controller,
showName: false,
),
)
..add(
DiagnosticsProperty<ScrollPhysics>(
'physics',
widget.physics,
showName: false,
),
)
..add(
FlagProperty(
'pageSnapping',
value: widget.pageSnapping,
ifFalse: 'snapping disabled',
),
)
..add(
FlagProperty(
'allowImplicitScrolling',
value: widget.allowImplicitScrolling,
ifTrue: 'allow implicit scrolling',
),
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,378 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui' show SemanticsRole;
import 'package:PiliPlus/common/widgets/flutter/page/page_view.dart';
import 'package:flutter/foundation.dart' show clampDouble;
import 'package:flutter/gestures.dart' show DragStartBehavior;
import 'package:flutter/material.dart' hide TabBarView, PageView;
/// A page view that displays the widget which corresponds to the currently
/// selected tab.
///
/// This widget is typically used in conjunction with a [TabBar].
///
/// {@youtube 560 315 https://www.youtube.com/watch?v=POtoEH-5l40}
///
/// If a [TabController] is not provided, then there must be a [DefaultTabController]
/// ancestor.
///
/// The tab controller's [TabController.length] must equal the length of the
/// [children] list and the length of the [TabBar.tabs] list.
///
/// To see a sample implementation, visit the [TabController] documentation.
class CustomTabBarView extends StatefulWidget {
/// Creates a page view with one child per tab.
///
/// The length of [children] must be the same as the [controller]'s length.
const CustomTabBarView({
super.key,
required this.children,
this.controller,
this.physics,
this.dragStartBehavior = DragStartBehavior.start,
this.viewportFraction = 1.0,
this.clipBehavior = Clip.hardEdge,
this.scrollDirection = Axis.horizontal,
this.header,
this.bgColor = Colors.transparent,
});
final Widget? header;
final Color bgColor;
/// This widget's selection and animation state.
///
/// If [TabController] is not provided, then the value of [DefaultTabController.of]
/// will be used.
final TabController? controller;
/// One widget per tab.
///
/// Its length must match the length of the [TabBar.tabs]
/// list, as well as the [controller]'s [TabController.length].
final List<Widget> children;
/// How the page view should respond to user input.
///
/// For example, determines how the page view continues to animate after the
/// user stops dragging the page view.
///
/// The physics are modified to snap to page boundaries using
/// [PageScrollPhysics] prior to being used.
///
/// Defaults to matching platform conventions.
final ScrollPhysics? physics;
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
final DragStartBehavior dragStartBehavior;
/// {@macro flutter.widgets.pageview.viewportFraction}
final double viewportFraction;
/// {@macro flutter.material.Material.clipBehavior}
///
/// Defaults to [Clip.hardEdge].
final Clip clipBehavior;
final Axis scrollDirection;
@override
State<CustomTabBarView> createState() => _CustomTabBarViewState();
}
class _CustomTabBarViewState extends State<CustomTabBarView> {
TabController? _controller;
PageController? _pageController;
late List<Widget> _childrenWithKey;
int? _currentIndex;
int _warpUnderwayCount = 0;
int _scrollUnderwayCount = 0;
bool _debugHasScheduledValidChildrenCountCheck = false;
// If the TabBarView is rebuilt with a new tab controller, the caller should
// dispose the old one. In that case the old controller's animation will be
// null and should not be accessed.
bool get _controllerIsValid => _controller?.animation != null;
void _updateTabController() {
final TabController? newController =
widget.controller ?? DefaultTabController.maybeOf(context);
assert(() {
if (newController == null) {
throw FlutterError(
'No TabController for ${widget.runtimeType}.\n'
'When creating a ${widget.runtimeType}, you must either provide an explicit '
'TabController using the "controller" property, or you must ensure that there '
'is a DefaultTabController above the ${widget.runtimeType}.\n'
'In this case, there was neither an explicit controller nor a default controller.',
);
}
return true;
}());
if (newController == _controller) {
return;
}
if (_controllerIsValid) {
_controller!.animation!.removeListener(_handleTabControllerAnimationTick);
}
_controller = newController;
if (_controller != null) {
_controller!.animation!.addListener(_handleTabControllerAnimationTick);
}
}
void _jumpToPage(int page) {
_warpUnderwayCount += 1;
_pageController!.jumpToPage(page);
_warpUnderwayCount -= 1;
}
Future<void> _animateToPage(
int page, {
required Duration duration,
required Curve curve,
}) async {
_warpUnderwayCount += 1;
await _pageController!.animateToPage(
page,
duration: duration,
curve: curve,
);
_warpUnderwayCount -= 1;
}
@override
void initState() {
super.initState();
_updateChildren();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_updateTabController();
_currentIndex = _controller!.index;
if (_pageController == null) {
_pageController = PageController(
initialPage: _currentIndex!,
viewportFraction: widget.viewportFraction,
);
} else {
_pageController!.jumpToPage(_currentIndex!);
}
}
@override
void didUpdateWidget(CustomTabBarView oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.controller != oldWidget.controller) {
_updateTabController();
_currentIndex = _controller!.index;
_jumpToPage(_currentIndex!);
}
if (widget.viewportFraction != oldWidget.viewportFraction) {
_pageController?.dispose();
_pageController = PageController(
initialPage: _currentIndex!,
viewportFraction: widget.viewportFraction,
);
}
// While a warp is under way, we stop updating the tab page contents.
// This is tracked in https://github.com/flutter/flutter/issues/31269.
if (widget.children != oldWidget.children && _warpUnderwayCount == 0) {
_updateChildren();
}
}
@override
void dispose() {
if (_controllerIsValid) {
_controller!.animation!.removeListener(_handleTabControllerAnimationTick);
}
_controller = null;
_pageController?.dispose();
// We don't own the _controller Animation, so it's not disposed here.
super.dispose();
}
void _updateChildren() {
_childrenWithKey = KeyedSubtree.ensureUniqueKeysForList(
widget.children.map<Widget>((Widget child) {
return Semantics(role: SemanticsRole.tabPanel, child: child);
}).toList(),
);
}
void _handleTabControllerAnimationTick() {
if (_scrollUnderwayCount > 0 || !_controller!.indexIsChanging) {
return;
} // This widget is driving the controller's animation.
if (_controller!.index != _currentIndex) {
_currentIndex = _controller!.index;
_warpToCurrentIndex();
}
}
void _warpToCurrentIndex() {
if (!mounted || _pageController!.page == _currentIndex!.toDouble()) {
return;
}
final bool adjacentDestination =
(_currentIndex! - _controller!.previousIndex).abs() == 1;
if (adjacentDestination) {
_warpToAdjacentTab(_controller!.animationDuration);
} else {
_warpToNonAdjacentTab(_controller!.animationDuration);
}
}
Future<void> _warpToAdjacentTab(Duration duration) async {
if (duration == Duration.zero) {
_jumpToPage(_currentIndex!);
} else {
await _animateToPage(
_currentIndex!,
duration: duration,
curve: Curves.ease,
);
}
if (mounted) {
setState(_updateChildren);
}
return Future<void>.value();
}
Future<void> _warpToNonAdjacentTab(Duration duration) async {
final int previousIndex = _controller!.previousIndex;
assert((_currentIndex! - previousIndex).abs() > 1);
// initialPage defines which page is shown when starting the animation.
// This page is adjacent to the destination page.
final int initialPage = _currentIndex! > previousIndex
? _currentIndex! - 1
: _currentIndex! + 1;
setState(() {
// Needed for `RenderSliverMultiBoxAdaptor.move` and kept alive children.
// For motivation, see https://github.com/flutter/flutter/pull/29188 and
// https://github.com/flutter/flutter/issues/27010#issuecomment-486475152.
_childrenWithKey = List<Widget>.of(_childrenWithKey, growable: false);
final Widget temp = _childrenWithKey[initialPage];
_childrenWithKey[initialPage] = _childrenWithKey[previousIndex];
_childrenWithKey[previousIndex] = temp;
});
// Make a first jump to the adjacent page.
_jumpToPage(initialPage);
// Jump or animate to the destination page.
if (duration == Duration.zero) {
_jumpToPage(_currentIndex!);
} else {
await _animateToPage(
_currentIndex!,
duration: duration,
curve: Curves.ease,
);
}
if (mounted) {
setState(_updateChildren);
}
}
void _syncControllerOffset() {
_controller!.offset = clampDouble(
_pageController!.page! - _controller!.index,
-1.0,
1.0,
);
}
// Called when the PageView scrolls
bool _handleScrollNotification(ScrollNotification notification) {
if (_warpUnderwayCount > 0 || _scrollUnderwayCount > 0) {
return false;
}
if (notification.depth != 0) {
return false;
}
if (!_controllerIsValid) {
return false;
}
_scrollUnderwayCount += 1;
final double page = _pageController!.page!;
if (notification is ScrollUpdateNotification &&
!_controller!.indexIsChanging) {
final bool pageChanged = (page - _controller!.index).abs() > 1.0;
if (pageChanged) {
_controller!.index = page.round();
_currentIndex = _controller!.index;
}
_syncControllerOffset();
} else if (notification is ScrollEndNotification) {
_controller!.index = page.round();
_currentIndex = _controller!.index;
if (!_controller!.indexIsChanging) {
_syncControllerOffset();
}
}
_scrollUnderwayCount -= 1;
return false;
}
bool _debugScheduleCheckHasValidChildrenCount() {
if (_debugHasScheduledValidChildrenCountCheck) {
return true;
}
WidgetsBinding.instance.addPostFrameCallback((Duration duration) {
_debugHasScheduledValidChildrenCountCheck = false;
if (!mounted) {
return;
}
assert(() {
if (_controller!.length != widget.children.length) {
throw FlutterError(
"Controller's length property (${_controller!.length}) does not match the "
"number of children (${widget.children.length}) present in TabBarView's children property.",
);
}
return true;
}());
}, debugLabel: 'TabBarView.validChildrenCountCheck');
_debugHasScheduledValidChildrenCountCheck = true;
return true;
}
@override
Widget build(BuildContext context) {
assert(_debugScheduleCheckHasValidChildrenCount());
return NotificationListener<ScrollNotification>(
onNotification: _handleScrollNotification,
child: CustomPageView(
scrollDirection: widget.scrollDirection,
dragStartBehavior: widget.dragStartBehavior,
clipBehavior: widget.clipBehavior,
controller: _pageController,
physics: widget.physics == null
? const PageScrollPhysics().applyTo(const ClampingScrollPhysics())
: const PageScrollPhysics().applyTo(widget.physics),
header: widget.header,
bgColor: widget.bgColor,
children: _childrenWithKey,
),
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,347 @@
import 'dart:ui' as ui;
import 'package:PiliPlus/common/widgets/flutter/text/paragraph.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart' hide RenderParagraph;
/// A paragraph of rich text.
///
/// {@youtube 560 315 https://www.youtube.com/watch?v=rykDVh-QFfw}
///
/// The [RichText] widget displays text that uses multiple different styles. The
/// text to display is described using a tree of [TextSpan] objects, each of
/// which has an associated style that is used for that subtree. The text might
/// break across multiple lines or might all be displayed on the same line
/// depending on the layout constraints.
///
/// Text displayed in a [RichText] widget must be explicitly styled. When
/// picking which style to use, consider using [DefaultTextStyle.of] the current
/// [BuildContext] to provide defaults. For more details on how to style text in
/// a [RichText] widget, see the documentation for [TextStyle].
///
/// Consider using the [Text] widget to integrate with the [DefaultTextStyle]
/// automatically. When all the text uses the same style, the default constructor
/// is less verbose. The [Text.rich] constructor allows you to style multiple
/// spans with the default text style while still allowing specified styles per
/// span.
///
/// {@tool snippet}
///
/// This sample demonstrates how to mix and match text with different text
/// styles using the [RichText] Widget. It displays the text "Hello bold world,"
/// emphasizing the word "bold" using a bold font weight.
///
/// ![](https://flutter.github.io/assets-for-api-docs/assets/widgets/rich_text.png)
///
/// ```dart
/// RichText(
/// text: TextSpan(
/// text: 'Hello ',
/// style: DefaultTextStyle.of(context).style,
/// children: const <TextSpan>[
/// TextSpan(text: 'bold', style: TextStyle(fontWeight: FontWeight.bold)),
/// TextSpan(text: ' world!'),
/// ],
/// ),
/// )
/// ```
/// {@end-tool}
///
/// ## Selections
///
/// To make this [RichText] Selectable, the [RichText] needs to be in the
/// subtree of a [SelectionArea] or [SelectableRegion] and a
/// [SelectionRegistrar] needs to be assigned to the
/// [RichText.selectionRegistrar]. One can use
/// [SelectionContainer.maybeOf] to get the [SelectionRegistrar] from a
/// context. This enables users to select the text in [RichText]s with mice or
/// touch events.
///
/// The [selectionColor] also needs to be set if the selection is enabled to
/// draw the selection highlights.
///
/// {@tool snippet}
///
/// This sample demonstrates how to assign a [SelectionRegistrar] for RichTexts
/// in the SelectionArea subtree.
///
/// ![](https://flutter.github.io/assets-for-api-docs/assets/widgets/rich_text.png)
///
/// ```dart
/// RichText(
/// text: const TextSpan(text: 'Hello'),
/// selectionRegistrar: SelectionContainer.maybeOf(context),
/// selectionColor: const Color(0xAF6694e8),
/// )
/// ```
/// {@end-tool}
///
/// See also:
///
/// * [TextStyle], which discusses how to style text.
/// * [TextSpan], which is used to describe the text in a paragraph.
/// * [Text], which automatically applies the ambient styles described by a
/// [DefaultTextStyle] to a single string.
/// * [Text.rich], a const text widget that provides similar functionality
/// as [RichText]. [Text.rich] will inherit [TextStyle] from [DefaultTextStyle].
/// * [SelectableRegion], which provides an overview of the selection system.
class RichText extends MultiChildRenderObjectWidget {
/// Creates a paragraph of rich text.
///
/// The [maxLines] property may be null (and indeed defaults to null), but if
/// it is not null, it must be greater than zero.
///
/// The [textDirection], if null, defaults to the ambient [Directionality],
/// which in that case must not be null.
RichText({
super.key,
required this.text,
this.textAlign = TextAlign.start,
this.textDirection,
this.softWrap = true,
this.overflow = TextOverflow.clip,
@Deprecated(
'Use textScaler instead. '
'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. '
'This feature was deprecated after v3.12.0-2.0.pre.',
)
double textScaleFactor = 1.0,
TextScaler textScaler = TextScaler.noScaling,
this.maxLines,
this.locale,
this.strutStyle,
this.textWidthBasis = TextWidthBasis.parent,
this.textHeightBehavior,
this.selectionRegistrar,
this.selectionColor,
this.onShowMore,
required this.primary,
}) : assert(maxLines == null || maxLines > 0),
assert(selectionRegistrar == null || selectionColor != null),
assert(
textScaleFactor == 1.0 || identical(textScaler, TextScaler.noScaling),
'Use textScaler instead.',
),
textScaler = _effectiveTextScalerFrom(textScaler, textScaleFactor),
super(
children: WidgetSpan.extractFromInlineSpan(
text,
_effectiveTextScalerFrom(textScaler, textScaleFactor),
),
);
static TextScaler _effectiveTextScalerFrom(
TextScaler textScaler,
double textScaleFactor,
) {
return switch ((textScaler, textScaleFactor)) {
(final TextScaler scaler, 1.0) => scaler,
(TextScaler.noScaling, final double textScaleFactor) => TextScaler.linear(
textScaleFactor,
),
(final TextScaler scaler, _) => scaler,
};
}
/// The text to display in this widget.
final InlineSpan text;
/// How the text should be aligned horizontally.
final TextAlign textAlign;
/// The directionality of the text.
///
/// This decides how [textAlign] values like [TextAlign.start] and
/// [TextAlign.end] are interpreted.
///
/// This is also used to disambiguate how to render bidirectional text. For
/// example, if the [text] is an English phrase followed by a Hebrew phrase,
/// in a [TextDirection.ltr] context the English phrase will be on the left
/// and the Hebrew phrase to its right, while in a [TextDirection.rtl]
/// context, the English phrase will be on the right and the Hebrew phrase on
/// its left.
///
/// Defaults to the ambient [Directionality], if any. If there is no ambient
/// [Directionality], then this must not be null.
final TextDirection? textDirection;
/// Whether the text should break at soft line breaks.
///
/// If false, the glyphs in the text will be positioned as if there was unlimited horizontal space.
final bool softWrap;
/// How visual overflow should be handled.
final TextOverflow overflow;
/// Deprecated. Will be removed in a future version of Flutter. Use
/// [textScaler] instead.
///
/// The number of font pixels for each logical pixel.
///
/// For example, if the text scale factor is 1.5, text will be 50% larger than
/// the specified font size.
@Deprecated(
'Use textScaler instead. '
'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. '
'This feature was deprecated after v3.12.0-2.0.pre.',
)
double get textScaleFactor => textScaler.textScaleFactor;
/// {@macro flutter.painting.textPainter.textScaler}
final TextScaler textScaler;
/// An optional maximum number of lines for the text to span, wrapping if necessary.
/// If the text exceeds the given number of lines, it will be truncated according
/// to [overflow].
///
/// If this is 1, text will not wrap. Otherwise, text will be wrapped at the
/// edge of the box.
final int? maxLines;
/// Used to select a font when the same Unicode character can
/// be rendered differently, depending on the locale.
///
/// It's rarely necessary to set this property. By default its value
/// is inherited from the enclosing app with `Localizations.localeOf(context)`.
///
/// See [RenderParagraph.locale] for more information.
final Locale? locale;
/// {@macro flutter.painting.textPainter.strutStyle}
final StrutStyle? strutStyle;
/// {@macro flutter.painting.textPainter.textWidthBasis}
final TextWidthBasis textWidthBasis;
/// {@macro dart.ui.textHeightBehavior}
final ui.TextHeightBehavior? textHeightBehavior;
/// The [SelectionRegistrar] this rich text is subscribed to.
///
/// If this is set, [selectionColor] must be non-null.
final SelectionRegistrar? selectionRegistrar;
/// The color to use when painting the selection.
///
/// This is ignored if [selectionRegistrar] is null.
///
/// See the section on selections in the [RichText] top-level API
/// documentation for more details on enabling selection in [RichText]
/// widgets.
final Color? selectionColor;
final Color primary;
final VoidCallback? onShowMore;
@override
RenderParagraph createRenderObject(BuildContext context) {
assert(textDirection != null || debugCheckHasDirectionality(context));
return RenderParagraph(
text,
textAlign: textAlign,
textDirection: textDirection ?? Directionality.of(context),
softWrap: softWrap,
overflow: overflow,
textScaler: textScaler,
maxLines: maxLines,
strutStyle: strutStyle,
textWidthBasis: textWidthBasis,
textHeightBehavior: textHeightBehavior,
locale: locale ?? Localizations.maybeLocaleOf(context),
registrar: selectionRegistrar,
selectionColor: selectionColor,
primary: primary,
onShowMore: onShowMore,
);
}
@override
void updateRenderObject(BuildContext context, RenderParagraph renderObject) {
assert(textDirection != null || debugCheckHasDirectionality(context));
renderObject
..text = text
..textAlign = textAlign
..textDirection = textDirection ?? Directionality.of(context)
..softWrap = softWrap
..overflow = overflow
..textScaler = textScaler
..maxLines = maxLines
..strutStyle = strutStyle
..textWidthBasis = textWidthBasis
..textHeightBehavior = textHeightBehavior
..locale = locale ?? Localizations.maybeLocaleOf(context)
..registrar = selectionRegistrar
..selectionColor = selectionColor
..primary = primary
..onShowMore = onShowMore;
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(
EnumProperty<TextAlign>(
'textAlign',
textAlign,
defaultValue: TextAlign.start,
),
);
properties.add(
EnumProperty<TextDirection>(
'textDirection',
textDirection,
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<TextOverflow>(
'overflow',
overflow,
defaultValue: TextOverflow.clip,
),
);
properties.add(
DiagnosticsProperty<TextScaler>(
'textScaler',
textScaler,
defaultValue: TextScaler.noScaling,
),
);
properties.add(IntProperty('maxLines', maxLines, ifNull: 'unlimited'));
properties.add(
EnumProperty<TextWidthBasis>(
'textWidthBasis',
textWidthBasis,
defaultValue: TextWidthBasis.parent,
),
);
properties.add(StringProperty('text', text.toPlainText()));
properties.add(
DiagnosticsProperty<Locale>('locale', locale, defaultValue: null),
);
properties.add(
DiagnosticsProperty<StrutStyle>(
'strutStyle',
strutStyle,
defaultValue: null,
),
);
properties.add(
DiagnosticsProperty<TextHeightBehavior>(
'textHeightBehavior',
textHeightBehavior,
defaultValue: null,
),
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,354 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// 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 'selectable_text.dart';
/// @docImport 'selection_area.dart';
/// @docImport 'text_field.dart';
library;
import 'package:PiliPlus/common/widgets/flutter/text_field/editable_text.dart';
import 'package:flutter/cupertino.dart' hide EditableText, EditableTextState;
import 'package:flutter/material.dart' hide EditableText, EditableTextState;
import 'package:flutter/rendering.dart';
/// The default context menu for text selection for the current platform.
///
/// {@template flutter.material.AdaptiveTextSelectionToolbar.contextMenuBuilders}
/// Typically, this widget would be passed to `contextMenuBuilder` in a
/// supported parent widget, such as:
///
/// * [EditableText.contextMenuBuilder]
/// * [TextField.contextMenuBuilder]
/// * [CupertinoTextField.contextMenuBuilder]
/// * [SelectionArea.contextMenuBuilder]
/// * [SelectableText.contextMenuBuilder]
/// {@endtemplate}
///
/// See also:
///
/// * [EditableText.getEditableButtonItems], which returns the default
/// [ContextMenuButtonItem]s for [EditableText] on the platform.
/// * [AdaptiveTextSelectionToolbar.getAdaptiveButtons], which builds the button
/// Widgets for the current platform given [ContextMenuButtonItem]s.
/// * [CupertinoAdaptiveTextSelectionToolbar], which does the same thing as this
/// widget but only for Cupertino context menus.
/// * [TextSelectionToolbar], the default toolbar for Android.
/// * [DesktopTextSelectionToolbar], the default toolbar for desktop platforms
/// other than MacOS.
/// * [CupertinoTextSelectionToolbar], the default toolbar for iOS.
/// * [CupertinoDesktopTextSelectionToolbar], the default toolbar for MacOS.
class AdaptiveTextSelectionToolbar extends StatelessWidget {
/// Create an instance of [AdaptiveTextSelectionToolbar] with the
/// given [children].
///
/// See also:
///
/// {@template flutter.material.AdaptiveTextSelectionToolbar.buttonItems}
/// * [AdaptiveTextSelectionToolbar.buttonItems], which takes a list of
/// [ContextMenuButtonItem]s instead of [children] widgets.
/// {@endtemplate}
/// {@template flutter.material.AdaptiveTextSelectionToolbar.editable}
/// * [AdaptiveTextSelectionToolbar.editable], which builds the default
/// children for an editable field.
/// {@endtemplate}
/// {@template flutter.material.AdaptiveTextSelectionToolbar.editableText}
/// * [AdaptiveTextSelectionToolbar.editableText], which builds the default
/// children for an [EditableText].
/// {@endtemplate}
/// {@template flutter.material.AdaptiveTextSelectionToolbar.selectable}
/// * [AdaptiveTextSelectionToolbar.selectable], which builds the default
/// children for content that is selectable but not editable.
/// {@endtemplate}
const AdaptiveTextSelectionToolbar({
super.key,
required this.children,
required this.anchors,
}) : buttonItems = null;
/// Create an instance of [AdaptiveTextSelectionToolbar] whose children will
/// be built from the given [buttonItems].
///
/// See also:
///
/// {@template flutter.material.AdaptiveTextSelectionToolbar.new}
/// * [AdaptiveTextSelectionToolbar.new], which takes the children directly as
/// a list of widgets.
/// {@endtemplate}
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.editable}
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.editableText}
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.selectable}
const AdaptiveTextSelectionToolbar.buttonItems({
super.key,
required this.buttonItems,
required this.anchors,
}) : children = null;
/// Create an instance of [AdaptiveTextSelectionToolbar] with the default
/// children for an editable field.
///
/// If an on* callback parameter is null, then its corresponding button will
/// not be built.
///
/// These callbacks are called when their corresponding button is activated
/// and only then. For example, `onPaste` is called when the user taps the
/// "Paste" button in the context menu and not when the user pastes with the
/// keyboard.
///
/// See also:
///
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.new}
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.editableText}
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.buttonItems}
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.selectable}
AdaptiveTextSelectionToolbar.editable({
super.key,
required ClipboardStatus clipboardStatus,
required VoidCallback? onCopy,
required VoidCallback? onCut,
required VoidCallback? onPaste,
required VoidCallback? onSelectAll,
required VoidCallback? onLookUp,
required VoidCallback? onSearchWeb,
required VoidCallback? onShare,
required VoidCallback? onLiveTextInput,
required this.anchors,
}) : children = null,
buttonItems = EditableText.getEditableButtonItems(
clipboardStatus: clipboardStatus,
onCopy: onCopy,
onCut: onCut,
onPaste: onPaste,
onSelectAll: onSelectAll,
onLookUp: onLookUp,
onSearchWeb: onSearchWeb,
onShare: onShare,
onLiveTextInput: onLiveTextInput,
);
/// Create an instance of [AdaptiveTextSelectionToolbar] with the default
/// children for an [EditableText].
///
/// See also:
///
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.new}
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.editable}
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.buttonItems}
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.selectable}
AdaptiveTextSelectionToolbar.editableText({
super.key,
required EditableTextState editableTextState,
}) : children = null,
buttonItems = editableTextState.contextMenuButtonItems,
anchors = editableTextState.contextMenuAnchors;
/// Create an instance of [AdaptiveTextSelectionToolbar] with the default
/// children for selectable, but not editable, content.
///
/// See also:
///
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.new}
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.buttonItems}
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.editable}
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.editableText}
AdaptiveTextSelectionToolbar.selectable({
super.key,
required VoidCallback onCopy,
required VoidCallback onSelectAll,
required VoidCallback? onShare,
required SelectionGeometry selectionGeometry,
required this.anchors,
}) : children = null,
buttonItems = SelectableRegion.getSelectableButtonItems(
selectionGeometry: selectionGeometry,
onCopy: onCopy,
onSelectAll: onSelectAll,
onShare: onShare,
);
/// Create an instance of [AdaptiveTextSelectionToolbar] with the default
/// children for a [SelectableRegion].
///
/// See also:
///
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.new}
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.buttonItems}
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.editable}
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.editableText}
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.selectable}
AdaptiveTextSelectionToolbar.selectableRegion({
super.key,
required SelectableRegionState selectableRegionState,
}) : children = null,
buttonItems = selectableRegionState.contextMenuButtonItems,
anchors = selectableRegionState.contextMenuAnchors;
/// {@template flutter.material.AdaptiveTextSelectionToolbar.buttonItems}
/// The [ContextMenuButtonItem]s that will be turned into the correct button
/// widgets for the current platform.
/// {@endtemplate}
final List<ContextMenuButtonItem>? buttonItems;
/// The children of the toolbar, typically buttons.
final List<Widget>? children;
/// {@template flutter.material.AdaptiveTextSelectionToolbar.anchors}
/// The location on which to anchor the menu.
/// {@endtemplate}
final TextSelectionToolbarAnchors anchors;
/// Returns the default button label String for the button of the given
/// [ContextMenuButtonType] on any platform.
static String getButtonLabel(
BuildContext context,
ContextMenuButtonItem buttonItem,
) {
if (buttonItem.label != null) {
return buttonItem.label!;
}
switch (Theme.of(context).platform) {
case TargetPlatform.iOS:
case TargetPlatform.macOS:
return CupertinoTextSelectionToolbarButton.getButtonLabel(
context,
buttonItem,
);
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
assert(debugCheckHasMaterialLocalizations(context));
final MaterialLocalizations localizations = MaterialLocalizations.of(
context,
);
return switch (buttonItem.type) {
ContextMenuButtonType.cut => localizations.cutButtonLabel,
ContextMenuButtonType.copy => localizations.copyButtonLabel,
ContextMenuButtonType.paste => localizations.pasteButtonLabel,
ContextMenuButtonType.selectAll => localizations.selectAllButtonLabel,
ContextMenuButtonType.delete =>
localizations.deleteButtonTooltip.toUpperCase(),
ContextMenuButtonType.lookUp => localizations.lookUpButtonLabel,
ContextMenuButtonType.searchWeb => localizations.searchWebButtonLabel,
ContextMenuButtonType.share => localizations.shareButtonLabel,
ContextMenuButtonType.liveTextInput =>
localizations.scanTextButtonLabel,
ContextMenuButtonType.custom => '',
};
}
}
/// Returns a List of Widgets generated by turning [buttonItems] into the
/// default context menu buttons for the current platform.
///
/// This is useful when building a text selection toolbar with the default
/// button appearance for the given platform, but where the toolbar and/or the
/// button actions and labels may be custom.
///
/// {@tool dartpad}
/// This sample demonstrates how to use `getAdaptiveButtons` to generate
/// default button widgets in a custom toolbar.
///
/// ** See code in examples/api/lib/material/context_menu/editable_text_toolbar_builder.2.dart **
/// {@end-tool}
///
/// See also:
///
/// * [CupertinoAdaptiveTextSelectionToolbar.getAdaptiveButtons], which is the
/// Cupertino equivalent of this class and builds only the Cupertino
/// buttons.
static Iterable<Widget> getAdaptiveButtons(
BuildContext context,
List<ContextMenuButtonItem> buttonItems,
) {
switch (Theme.of(context).platform) {
case TargetPlatform.iOS:
return buttonItems.map((ContextMenuButtonItem buttonItem) {
return CupertinoTextSelectionToolbarButton.buttonItem(
buttonItem: buttonItem,
);
});
case TargetPlatform.fuchsia:
case TargetPlatform.android:
final List<Widget> buttons = <Widget>[];
for (int i = 0; i < buttonItems.length; i++) {
final ContextMenuButtonItem buttonItem = buttonItems[i];
buttons.add(
TextSelectionToolbarTextButton(
padding: TextSelectionToolbarTextButton.getPadding(
i,
buttonItems.length,
),
onPressed: buttonItem.onPressed,
alignment: AlignmentDirectional.centerStart,
child: Text(getButtonLabel(context, buttonItem)),
),
);
}
return buttons;
case TargetPlatform.linux:
case TargetPlatform.windows:
return buttonItems.map((ContextMenuButtonItem buttonItem) {
return DesktopTextSelectionToolbarButton.text(
context: context,
onPressed: buttonItem.onPressed,
text: getButtonLabel(context, buttonItem),
);
});
case TargetPlatform.macOS:
return buttonItems.map((ContextMenuButtonItem buttonItem) {
return CupertinoDesktopTextSelectionToolbarButton.text(
onPressed: buttonItem.onPressed,
text: getButtonLabel(context, buttonItem),
);
});
}
}
@override
Widget build(BuildContext context) {
// If there aren't any buttons to build, build an empty toolbar.
if ((children != null && children!.isEmpty) ||
(buttonItems != null && buttonItems!.isEmpty)) {
return const SizedBox.shrink();
}
final List<Widget> resultChildren = children != null
? children!
: getAdaptiveButtons(context, buttonItems!).toList();
switch (Theme.of(context).platform) {
case TargetPlatform.iOS:
return CupertinoTextSelectionToolbar(
anchorAbove: anchors.primaryAnchor,
anchorBelow: anchors.secondaryAnchor == null
? anchors.primaryAnchor
: anchors.secondaryAnchor!,
children: resultChildren,
);
case TargetPlatform.android:
return TextSelectionToolbar(
anchorAbove: anchors.primaryAnchor,
anchorBelow: anchors.secondaryAnchor == null
? anchors.primaryAnchor
: anchors.secondaryAnchor!,
children: resultChildren,
);
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
return DesktopTextSelectionToolbar(
anchor: anchors.primaryAnchor,
children: resultChildren,
);
case TargetPlatform.macOS:
return CupertinoDesktopTextSelectionToolbar(
anchor: anchors.primaryAnchor,
children: resultChildren,
);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,237 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/// @docImport 'package:flutter/material.dart';
library;
import 'package:PiliPlus/common/widgets/flutter/text_field/editable_text.dart';
import 'package:flutter/cupertino.dart' hide EditableText, EditableTextState;
import 'package:flutter/foundation.dart' show defaultTargetPlatform;
import 'package:flutter/rendering.dart';
/// The default Cupertino context menu for text selection for the current
/// platform with the given children.
///
/// {@template flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.platforms}
/// Builds the mobile Cupertino context menu on all mobile platforms, not just
/// iOS, and builds the desktop Cupertino context menu on all desktop platforms,
/// not just MacOS. For a widget that builds the native-looking context menu for
/// all platforms, see [AdaptiveTextSelectionToolbar].
/// {@endtemplate}
///
/// See also:
///
/// * [AdaptiveTextSelectionToolbar], which does the same thing as this widget
/// but for all platforms, not just the Cupertino-styled platforms.
/// * [CupertinoAdaptiveTextSelectionToolbar.getAdaptiveButtons], which builds
/// the Cupertino button Widgets for the current platform given
/// [ContextMenuButtonItem]s.
class CupertinoAdaptiveTextSelectionToolbar extends StatelessWidget {
/// Create an instance of [CupertinoAdaptiveTextSelectionToolbar] with the
/// given [children].
///
/// See also:
///
/// {@template flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.buttonItems}
/// * [CupertinoAdaptiveTextSelectionToolbar.buttonItems], which takes a list
/// of [ContextMenuButtonItem]s instead of [children] widgets.
/// {@endtemplate}
/// {@template flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.editable}
/// * [CupertinoAdaptiveTextSelectionToolbar.editable], which builds the
/// default Cupertino children for an editable field.
/// {@endtemplate}
/// {@template flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.editableText}
/// * [CupertinoAdaptiveTextSelectionToolbar.editableText], which builds the
/// default Cupertino children for an [EditableText].
/// {@endtemplate}
/// {@template flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.selectable}
/// * [CupertinoAdaptiveTextSelectionToolbar.selectable], which builds the
/// Cupertino children for content that is selectable but not editable.
/// {@endtemplate}
const CupertinoAdaptiveTextSelectionToolbar({
super.key,
required this.children,
required this.anchors,
}) : buttonItems = null;
/// Create an instance of [CupertinoAdaptiveTextSelectionToolbar] whose
/// children will be built from the given [buttonItems].
///
/// See also:
///
/// {@template flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.new}
/// * [CupertinoAdaptiveTextSelectionToolbar.new], which takes the children
/// directly as a list of widgets.
/// {@endtemplate}
/// {@macro flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.editable}
/// {@macro flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.editableText}
/// {@macro flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.selectable}
const CupertinoAdaptiveTextSelectionToolbar.buttonItems({
super.key,
required this.buttonItems,
required this.anchors,
}) : children = null;
/// Create an instance of [CupertinoAdaptiveTextSelectionToolbar] with the
/// default children for an editable field.
///
/// If a callback is null, then its corresponding button will not be built.
///
/// See also:
///
/// * [AdaptiveTextSelectionToolbar.editable], which is similar to this but
/// includes Material and Cupertino toolbars.
/// {@macro flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.new}
/// {@macro flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.editableText}
/// {@macro flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.buttonItems}
/// {@macro flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.selectable}
CupertinoAdaptiveTextSelectionToolbar.editable({
super.key,
required ClipboardStatus clipboardStatus,
required VoidCallback? onCopy,
required VoidCallback? onCut,
required VoidCallback? onPaste,
required VoidCallback? onSelectAll,
required VoidCallback? onLookUp,
required VoidCallback? onSearchWeb,
required VoidCallback? onShare,
required VoidCallback? onLiveTextInput,
required this.anchors,
}) : children = null,
buttonItems = EditableText.getEditableButtonItems(
clipboardStatus: clipboardStatus,
onCopy: onCopy,
onCut: onCut,
onPaste: onPaste,
onSelectAll: onSelectAll,
onLookUp: onLookUp,
onSearchWeb: onSearchWeb,
onShare: onShare,
onLiveTextInput: onLiveTextInput,
);
/// Create an instance of [CupertinoAdaptiveTextSelectionToolbar] with the
/// default children for an [EditableText].
///
/// See also:
///
/// * [AdaptiveTextSelectionToolbar.editableText], which is similar to this
/// but includes Material and Cupertino toolbars.
/// {@macro flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.new}
/// {@macro flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.editable}
/// {@macro flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.buttonItems}
/// {@macro flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.selectable}
CupertinoAdaptiveTextSelectionToolbar.editableText({
super.key,
required EditableTextState editableTextState,
}) : children = null,
buttonItems = editableTextState.contextMenuButtonItems,
anchors = editableTextState.contextMenuAnchors;
/// Create an instance of [CupertinoAdaptiveTextSelectionToolbar] with the
/// default children for selectable, but not editable, content.
///
/// See also:
///
/// * [AdaptiveTextSelectionToolbar.selectable], which is similar to this but
/// includes Material and Cupertino toolbars.
/// {@macro flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.new}
/// {@macro flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.buttonItems}
/// {@macro flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.editable}
/// {@macro flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.editableText}
CupertinoAdaptiveTextSelectionToolbar.selectable({
super.key,
required VoidCallback onCopy,
required VoidCallback onSelectAll,
required SelectionGeometry selectionGeometry,
required this.anchors,
}) : children = null,
buttonItems = SelectableRegion.getSelectableButtonItems(
selectionGeometry: selectionGeometry,
onCopy: onCopy,
onSelectAll: onSelectAll,
onShare: null, // See https://github.com/flutter/flutter/issues/141775.
);
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.anchors}
final TextSelectionToolbarAnchors anchors;
/// The children of the toolbar, typically buttons.
final List<Widget>? children;
/// The [ContextMenuButtonItem]s that will be turned into the correct button
/// widgets for the current platform.
final List<ContextMenuButtonItem>? buttonItems;
/// Returns a List of Widgets generated by turning [buttonItems] into the
/// default context menu buttons for Cupertino on the current platform.
///
/// This is useful when building a text selection toolbar with the default
/// button appearance for the given platform, but where the toolbar and/or the
/// button actions and labels may be custom.
///
/// Does not build Material buttons. On non-Apple platforms, Cupertino buttons
/// will still be used, because the Cupertino library does not access the
/// Material library. To get the native-looking buttons on every platform,
/// use [AdaptiveTextSelectionToolbar.getAdaptiveButtons] in the Material
/// library.
///
/// See also:
///
/// * [AdaptiveTextSelectionToolbar.getAdaptiveButtons], which is the Material
/// equivalent of this class and builds only the Material buttons. It
/// includes a live example of using `getAdaptiveButtons`.
static Iterable<Widget> getAdaptiveButtons(
BuildContext context,
List<ContextMenuButtonItem> buttonItems,
) {
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.iOS:
return buttonItems.map((ContextMenuButtonItem buttonItem) {
return CupertinoTextSelectionToolbarButton.buttonItem(
buttonItem: buttonItem,
);
});
case TargetPlatform.linux:
case TargetPlatform.windows:
case TargetPlatform.macOS:
return buttonItems.map((ContextMenuButtonItem buttonItem) {
return CupertinoDesktopTextSelectionToolbarButton.buttonItem(
buttonItem: buttonItem,
);
});
}
}
@override
Widget build(BuildContext context) {
// If there aren't any buttons to build, build an empty toolbar.
if ((children?.isEmpty ?? false) || (buttonItems?.isEmpty ?? false)) {
return const SizedBox.shrink();
}
final List<Widget> resultChildren =
children ?? getAdaptiveButtons(context, buttonItems!).toList();
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.iOS:
case TargetPlatform.fuchsia:
return CupertinoTextSelectionToolbar(
anchorAbove: anchors.primaryAnchor,
anchorBelow: anchors.secondaryAnchor ?? anchors.primaryAnchor,
children: resultChildren,
);
case TargetPlatform.linux:
case TargetPlatform.windows:
case TargetPlatform.macOS:
return CupertinoDesktopTextSelectionToolbar(
anchor: anchors.primaryAnchor,
children: resultChildren,
);
}
}
}

View File

@@ -0,0 +1,176 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/// @docImport 'package:flutter/material.dart';
library;
import 'package:PiliPlus/common/widgets/flutter/text_field/editable_text.dart';
import 'package:flutter/cupertino.dart' hide EditableText, EditableTextState;
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart'
show SelectionChangedCause, SuggestionSpan;
/// iOS only shows 3 spell check suggestions in the toolbar.
const int _kMaxSuggestions = 3;
/// The default spell check suggestions toolbar for iOS.
///
/// Tries to position itself below the [anchors], but if it doesn't fit, then it
/// readjusts to fit above bottom view insets.
///
/// See also:
/// * [SpellCheckSuggestionsToolbar], which is similar but for both the
/// Material and Cupertino libraries.
class CupertinoSpellCheckSuggestionsToolbar extends StatelessWidget {
/// Constructs a [CupertinoSpellCheckSuggestionsToolbar].
///
/// [buttonItems] must not contain more than three items.
const CupertinoSpellCheckSuggestionsToolbar({
super.key,
required this.anchors,
required this.buttonItems,
}) : assert(buttonItems.length <= _kMaxSuggestions);
/// Constructs a [CupertinoSpellCheckSuggestionsToolbar] with the default
/// children for an [EditableText].
///
/// See also:
/// * [SpellCheckSuggestionsToolbar.editableText], which is similar but
/// builds an Android-style toolbar.
CupertinoSpellCheckSuggestionsToolbar.editableText({
super.key,
required EditableTextState editableTextState,
}) : buttonItems =
buildButtonItems(editableTextState) ?? <ContextMenuButtonItem>[],
anchors = editableTextState.contextMenuAnchors;
/// The location on which to anchor the menu.
final TextSelectionToolbarAnchors anchors;
/// The [ContextMenuButtonItem]s that will be turned into the correct button
/// widgets and displayed in the spell check suggestions toolbar.
///
/// Must not contain more than three items.
///
/// See also:
///
/// * [AdaptiveTextSelectionToolbar.buttonItems], the list of
/// [ContextMenuButtonItem]s that are used to build the buttons of the
/// text selection toolbar.
/// * [SpellCheckSuggestionsToolbar.buttonItems], the list of
/// [ContextMenuButtonItem]s used to build the Material style spell check
/// suggestions toolbar.
final List<ContextMenuButtonItem> buttonItems;
/// Builds the button items for the toolbar based on the available
/// spell check suggestions.
static List<ContextMenuButtonItem>? buildButtonItems(
EditableTextState editableTextState,
) {
// Determine if composing region is misspelled.
final SuggestionSpan? spanAtCursorIndex = editableTextState
.findSuggestionSpanAtCursorIndex(
editableTextState.currentTextEditingValue.selection.baseOffset,
);
if (spanAtCursorIndex == null) {
return null;
}
if (spanAtCursorIndex.suggestions.isEmpty) {
assert(debugCheckHasCupertinoLocalizations(editableTextState.context));
final CupertinoLocalizations localizations = CupertinoLocalizations.of(
editableTextState.context,
);
return <ContextMenuButtonItem>[
ContextMenuButtonItem(
onPressed: null,
label: localizations.noSpellCheckReplacementsLabel,
),
];
}
final List<ContextMenuButtonItem> buttonItems = <ContextMenuButtonItem>[];
// Build suggestion buttons.
for (final String suggestion in spanAtCursorIndex.suggestions.take(
_kMaxSuggestions,
)) {
buttonItems.add(
ContextMenuButtonItem(
onPressed: () {
if (!editableTextState.mounted) {
return;
}
_replaceText(
editableTextState,
suggestion,
spanAtCursorIndex.range,
);
},
label: suggestion,
),
);
}
return buttonItems;
}
static void _replaceText(
EditableTextState editableTextState,
String text,
TextRange replacementRange,
) {
// Replacement cannot be performed if the text is read only or obscured.
assert(
!editableTextState.widget.readOnly &&
!editableTextState.widget.obscureText,
);
final TextEditingValue newValue = editableTextState.textEditingValue
.replaced(replacementRange, text)
.copyWith(
selection: TextSelection.collapsed(
offset: replacementRange.start + text.length,
),
);
editableTextState.userUpdateTextEditingValue(
newValue,
SelectionChangedCause.toolbar,
);
// Schedule a call to bringIntoView() after renderEditable updates.
SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
if (editableTextState.mounted) {
editableTextState.bringIntoView(
editableTextState.textEditingValue.selection.extent,
);
}
}, debugLabel: 'SpellCheckSuggestions.bringIntoView');
editableTextState.hideToolbar();
}
/// Builds the toolbar buttons based on the [buttonItems].
List<Widget> _buildToolbarButtons(BuildContext context) {
return buttonItems.map((ContextMenuButtonItem buttonItem) {
return CupertinoTextSelectionToolbarButton.buttonItem(
buttonItem: buttonItem,
);
}).toList();
}
@override
Widget build(BuildContext context) {
if (buttonItems.isEmpty) {
return const SizedBox.shrink();
}
final List<Widget> children = _buildToolbarButtons(context);
return CupertinoTextSelectionToolbar(
anchorAbove: anchors.primaryAnchor,
anchorBelow: anchors.secondaryAnchor == null
? anchors.primaryAnchor
: anchors.secondaryAnchor!,
children: children,
);
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,471 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/// @docImport 'editable_text.dart';
library;
import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart';
import 'package:flutter/services.dart'
show SpellCheckResults, SpellCheckService, SuggestionSpan, TextEditingValue;
import 'package:PiliPlus/common/widgets/flutter/text_field/editable_text.dart'
show EditableTextContextMenuBuilder;
/// Controls how spell check is performed for text input.
///
/// This configuration determines the [SpellCheckService] used to fetch the
/// [List<SuggestionSpan>] spell check results and the [TextStyle] used to
/// mark misspelled words within text input.
@immutable
class SpellCheckConfiguration {
/// Creates a configuration that specifies the service and suggestions handler
/// for spell check.
const SpellCheckConfiguration({
this.spellCheckService,
this.misspelledSelectionColor,
this.misspelledTextStyle,
this.spellCheckSuggestionsToolbarBuilder,
}) : _spellCheckEnabled = true;
/// Creates a configuration that disables spell check.
const SpellCheckConfiguration.disabled()
: _spellCheckEnabled = false,
spellCheckService = null,
spellCheckSuggestionsToolbarBuilder = null,
misspelledTextStyle = null,
misspelledSelectionColor = null;
/// The service used to fetch spell check results for text input.
final SpellCheckService? spellCheckService;
/// The color the paint the selection highlight when spell check is showing
/// suggestions for a misspelled word.
///
/// For example, on iOS, the selection appears red while the spell check menu
/// is showing.
final Color? misspelledSelectionColor;
/// Style used to indicate misspelled words.
///
/// This is nullable to allow style-specific wrappers of [EditableText]
/// to infer this, but this must be specified if this configuration is
/// provided directly to [EditableText] or its construction will fail with an
/// assertion error.
final TextStyle? misspelledTextStyle;
/// Builds the toolbar used to display spell check suggestions for misspelled
/// words.
final EditableTextContextMenuBuilder? spellCheckSuggestionsToolbarBuilder;
final bool _spellCheckEnabled;
/// Whether or not the configuration should enable or disable spell check.
bool get spellCheckEnabled => _spellCheckEnabled;
/// Returns a copy of the current [SpellCheckConfiguration] instance with
/// specified overrides.
SpellCheckConfiguration copyWith({
SpellCheckService? spellCheckService,
Color? misspelledSelectionColor,
TextStyle? misspelledTextStyle,
EditableTextContextMenuBuilder? spellCheckSuggestionsToolbarBuilder,
}) {
if (!_spellCheckEnabled) {
// A new configuration should be constructed to enable spell check.
return const SpellCheckConfiguration.disabled();
}
return SpellCheckConfiguration(
spellCheckService: spellCheckService ?? this.spellCheckService,
misspelledSelectionColor:
misspelledSelectionColor ?? this.misspelledSelectionColor,
misspelledTextStyle: misspelledTextStyle ?? this.misspelledTextStyle,
spellCheckSuggestionsToolbarBuilder:
spellCheckSuggestionsToolbarBuilder ??
this.spellCheckSuggestionsToolbarBuilder,
);
}
@override
String toString() {
return '${objectRuntimeType(this, 'SpellCheckConfiguration')}('
'${_spellCheckEnabled ? 'enabled' : 'disabled'}, '
'service: $spellCheckService, '
'text style: $misspelledTextStyle, '
'toolbar builder: $spellCheckSuggestionsToolbarBuilder'
')';
}
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) {
return false;
}
return other is SpellCheckConfiguration &&
other.spellCheckService == spellCheckService &&
other.misspelledTextStyle == misspelledTextStyle &&
other.spellCheckSuggestionsToolbarBuilder ==
spellCheckSuggestionsToolbarBuilder &&
other._spellCheckEnabled == _spellCheckEnabled;
}
@override
int get hashCode => Object.hash(
spellCheckService,
misspelledTextStyle,
spellCheckSuggestionsToolbarBuilder,
_spellCheckEnabled,
);
}
// Methods for displaying spell check results:
/// Adjusts spell check results to correspond to [newText] if the only results
/// that the handler has access to are the [results] corresponding to
/// [resultsText].
///
/// Used in the case where the request for the spell check results of the
/// [newText] is lagging in order to avoid display of incorrect results.
List<SuggestionSpan> _correctSpellCheckResults(
String newText,
String resultsText,
List<SuggestionSpan> results,
) {
final List<SuggestionSpan> correctedSpellCheckResults = <SuggestionSpan>[];
int spanPointer = 0;
int offset = 0;
// Assumes that the order of spans has not been jumbled for optimization
// purposes, and will only search since the previously found span.
int searchStart = 0;
while (spanPointer < results.length) {
final SuggestionSpan currentSpan = results[spanPointer];
final String currentSpanText = resultsText.substring(
currentSpan.range.start,
currentSpan.range.end,
);
final int spanLength = currentSpan.range.end - currentSpan.range.start;
// Try finding SuggestionSpan from resultsText in new text.
final String escapedText = RegExp.escape(currentSpanText);
final RegExp currentSpanTextRegexp = RegExp('\\b$escapedText\\b');
final int foundIndex = newText
.substring(searchStart)
.indexOf(currentSpanTextRegexp);
// Check whether word was found exactly where expected or elsewhere in the newText.
final bool currentSpanFoundExactly =
currentSpan.range.start == foundIndex + searchStart;
final bool currentSpanFoundExactlyWithOffset =
currentSpan.range.start + offset == foundIndex + searchStart;
final bool currentSpanFoundElsewhere = foundIndex >= 0;
if (currentSpanFoundExactly || currentSpanFoundExactlyWithOffset) {
// currentSpan was found at the same index in newText and resultsText
// or at the same index with the previously calculated adjustment by
// the offset value, so apply it to new text by adding it to the list of
// corrected results.
final SuggestionSpan adjustedSpan = SuggestionSpan(
TextRange(
start: currentSpan.range.start + offset,
end: currentSpan.range.end + offset,
),
currentSpan.suggestions,
);
// Start search for the next misspelled word at the end of currentSpan.
searchStart = math.min(
currentSpan.range.end + 1 + offset,
newText.length,
);
correctedSpellCheckResults.add(adjustedSpan);
} else if (currentSpanFoundElsewhere) {
// Word was pushed forward but not modified.
final int adjustedSpanStart = searchStart + foundIndex;
final int adjustedSpanEnd = adjustedSpanStart + spanLength;
final SuggestionSpan adjustedSpan = SuggestionSpan(
TextRange(start: adjustedSpanStart, end: adjustedSpanEnd),
currentSpan.suggestions,
);
// Start search for the next misspelled word at the end of the
// adjusted currentSpan.
searchStart = math.min(adjustedSpanEnd + 1, newText.length);
// Adjust offset to reflect the difference between where currentSpan
// was positioned in resultsText versus in newText.
offset = adjustedSpanStart - currentSpan.range.start;
correctedSpellCheckResults.add(adjustedSpan);
}
spanPointer++;
}
return correctedSpellCheckResults;
}
/// Builds the [TextSpan] tree given the current state of the text input and
/// spell check results.
///
/// The [value] is the current [TextEditingValue] requested to be rendered
/// by a text input widget. The [composingWithinCurrentTextRange] value
/// represents whether or not there is a valid composing region in the
/// [value]. The [style] is the [TextStyle] to render the [value]'s text with,
/// and the [misspelledTextStyle] is the [TextStyle] to render misspelled
/// words within the [value]'s text with. The [spellCheckResults] are the
/// results of spell checking the [value]'s text.
TextSpan buildTextSpanWithSpellCheckSuggestions(
TextEditingValue value,
bool composingWithinCurrentTextRange,
TextStyle? style,
TextStyle misspelledTextStyle,
SpellCheckResults spellCheckResults,
) {
List<SuggestionSpan> spellCheckResultsSpans =
spellCheckResults.suggestionSpans;
final String spellCheckResultsText = spellCheckResults.spellCheckedText;
if (spellCheckResultsText != value.text) {
spellCheckResultsSpans = _correctSpellCheckResults(
value.text,
spellCheckResultsText,
spellCheckResultsSpans,
);
}
// We will draw the TextSpan tree based on the composing region, if it is
// available.
// TODO(camsim99): The two separate strategies for building TextSpan trees
// based on the availability of a composing region should be merged:
// https://github.com/flutter/flutter/issues/124142.
final bool shouldConsiderComposingRegion =
defaultTargetPlatform == TargetPlatform.android;
if (shouldConsiderComposingRegion) {
return TextSpan(
style: style,
children: _buildSubtreesWithComposingRegion(
spellCheckResultsSpans,
value,
style,
misspelledTextStyle,
composingWithinCurrentTextRange,
),
);
}
return TextSpan(
style: style,
children: _buildSubtreesWithoutComposingRegion(
spellCheckResultsSpans,
value,
style,
misspelledTextStyle,
value.selection.baseOffset,
),
);
}
/// Builds the [TextSpan] tree for spell check without considering the composing
/// region. Instead, uses the cursor to identify the word that's actively being
/// edited and shouldn't be spell checked. This is useful for platforms and IMEs
/// that don't use the composing region for the active word.
List<TextSpan> _buildSubtreesWithoutComposingRegion(
List<SuggestionSpan>? spellCheckSuggestions,
TextEditingValue value,
TextStyle? style,
TextStyle misspelledStyle,
int cursorIndex,
) {
final List<TextSpan> textSpanTreeChildren = <TextSpan>[];
int textPointer = 0;
int currentSpanPointer = 0;
int endIndex;
final String text = value.text;
final TextStyle misspelledJointStyle =
style?.merge(misspelledStyle) ?? misspelledStyle;
bool cursorInCurrentSpan = false;
// Add text interwoven with any misspelled words to the tree.
if (spellCheckSuggestions != null) {
while (textPointer < text.length &&
currentSpanPointer < spellCheckSuggestions.length) {
final SuggestionSpan currentSpan =
spellCheckSuggestions[currentSpanPointer];
if (currentSpan.range.start > textPointer) {
endIndex = currentSpan.range.start < text.length
? currentSpan.range.start
: text.length;
textSpanTreeChildren.add(
TextSpan(style: style, text: text.substring(textPointer, endIndex)),
);
textPointer = endIndex;
} else {
endIndex = currentSpan.range.end < text.length
? currentSpan.range.end
: text.length;
cursorInCurrentSpan =
currentSpan.range.start <= cursorIndex &&
currentSpan.range.end >= cursorIndex;
textSpanTreeChildren.add(
TextSpan(
style: cursorInCurrentSpan ? style : misspelledJointStyle,
text: text.substring(currentSpan.range.start, endIndex),
),
);
textPointer = endIndex;
currentSpanPointer++;
}
}
}
// Add any remaining text to the tree if applicable.
if (textPointer < text.length) {
textSpanTreeChildren.add(
TextSpan(style: style, text: text.substring(textPointer, text.length)),
);
}
return textSpanTreeChildren;
}
/// Builds [TextSpan] subtree for text with misspelled words with logic based on
/// a valid composing region.
List<TextSpan> _buildSubtreesWithComposingRegion(
List<SuggestionSpan>? spellCheckSuggestions,
TextEditingValue value,
TextStyle? style,
TextStyle misspelledStyle,
bool composingWithinCurrentTextRange,
) {
final List<TextSpan> textSpanTreeChildren = <TextSpan>[];
int textPointer = 0;
int currentSpanPointer = 0;
int endIndex;
SuggestionSpan currentSpan;
final String text = value.text;
final TextRange composingRegion = value.composing;
final TextStyle composingTextStyle =
style?.merge(const TextStyle(decoration: TextDecoration.underline)) ??
const TextStyle(decoration: TextDecoration.underline);
final TextStyle misspelledJointStyle =
style?.merge(misspelledStyle) ?? misspelledStyle;
bool textPointerWithinComposingRegion = false;
bool currentSpanIsComposingRegion = false;
// Add text interwoven with any misspelled words to the tree.
if (spellCheckSuggestions != null) {
while (textPointer < text.length &&
currentSpanPointer < spellCheckSuggestions.length) {
currentSpan = spellCheckSuggestions[currentSpanPointer];
if (currentSpan.range.start > textPointer) {
endIndex = currentSpan.range.start < text.length
? currentSpan.range.start
: text.length;
textPointerWithinComposingRegion =
composingRegion.start >= textPointer &&
composingRegion.end <= endIndex &&
!composingWithinCurrentTextRange;
if (textPointerWithinComposingRegion) {
_addComposingRegionTextSpans(
textSpanTreeChildren,
text,
textPointer,
composingRegion,
style,
composingTextStyle,
);
textSpanTreeChildren.add(
TextSpan(
style: style,
text: text.substring(composingRegion.end, endIndex),
),
);
} else {
textSpanTreeChildren.add(
TextSpan(style: style, text: text.substring(textPointer, endIndex)),
);
}
textPointer = endIndex;
} else {
endIndex = currentSpan.range.end < text.length
? currentSpan.range.end
: text.length;
currentSpanIsComposingRegion =
textPointer >= composingRegion.start &&
endIndex <= composingRegion.end &&
!composingWithinCurrentTextRange;
textSpanTreeChildren.add(
TextSpan(
style: currentSpanIsComposingRegion
? composingTextStyle
: misspelledJointStyle,
text: text.substring(currentSpan.range.start, endIndex),
),
);
textPointer = endIndex;
currentSpanPointer++;
}
}
}
// Add any remaining text to the tree if applicable.
if (textPointer < text.length) {
if (textPointer < composingRegion.start &&
!composingWithinCurrentTextRange) {
_addComposingRegionTextSpans(
textSpanTreeChildren,
text,
textPointer,
composingRegion,
style,
composingTextStyle,
);
if (composingRegion.end != text.length) {
textSpanTreeChildren.add(
TextSpan(
style: style,
text: text.substring(composingRegion.end, text.length),
),
);
}
} else {
textSpanTreeChildren.add(
TextSpan(style: style, text: text.substring(textPointer, text.length)),
);
}
}
return textSpanTreeChildren;
}
/// Helper method to create [TextSpan] tree children for specified range of
/// text up to and including the composing region.
void _addComposingRegionTextSpans(
List<TextSpan> treeChildren,
String text,
int start,
TextRange composingRegion,
TextStyle? style,
TextStyle composingTextStyle,
) {
treeChildren.add(
TextSpan(style: style, text: text.substring(start, composingRegion.start)),
);
treeChildren.add(
TextSpan(
style: composingTextStyle,
text: text.substring(composingRegion.start, composingRegion.end),
),
);
}

View File

@@ -0,0 +1,279 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:PiliPlus/common/widgets/flutter/text_field/editable_text.dart';
import 'package:flutter/cupertino.dart' hide EditableText, EditableTextState;
import 'package:flutter/material.dart' hide EditableText, EditableTextState;
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart'
show SelectionChangedCause, SuggestionSpan;
// The default height of the SpellCheckSuggestionsToolbar, which
// assumes there are the maximum number of spell check suggestions available, 3.
// Size eyeballed on Pixel 4 emulator running Android API 31.
const double _kDefaultToolbarHeight = 193.0;
/// The maximum number of suggestions in the toolbar is 3, plus a delete button.
const int _kMaxSuggestions = 3;
/// The default spell check suggestions toolbar for Android.
///
/// Tries to position itself below the [anchor], but if it doesn't fit, then it
/// readjusts to fit above bottom view insets.
///
/// See also:
///
/// * [CupertinoSpellCheckSuggestionsToolbar], which is similar but builds an
/// iOS-style spell check toolbar.
class SpellCheckSuggestionsToolbar extends StatelessWidget {
/// Constructs a [SpellCheckSuggestionsToolbar].
///
/// [buttonItems] must not contain more than four items, generally three
/// suggestions and one delete button.
const SpellCheckSuggestionsToolbar({
super.key,
required this.anchor,
required this.buttonItems,
}) : assert(buttonItems.length <= _kMaxSuggestions + 1);
/// Constructs a [SpellCheckSuggestionsToolbar] with the default children for
/// an [EditableText].
///
/// See also:
/// * [CupertinoSpellCheckSuggestionsToolbar.editableText], which is similar
/// but builds an iOS-style toolbar.
SpellCheckSuggestionsToolbar.editableText({
super.key,
required EditableTextState editableTextState,
}) : buttonItems =
buildButtonItems(editableTextState) ?? <ContextMenuButtonItem>[],
anchor = getToolbarAnchor(editableTextState.contextMenuAnchors);
/// {@template flutter.material.SpellCheckSuggestionsToolbar.anchor}
/// The focal point below which the toolbar attempts to position itself.
/// {@endtemplate}
final Offset anchor;
/// The [ContextMenuButtonItem]s that will be turned into the correct button
/// widgets and displayed in the spell check suggestions toolbar.
///
/// Must not contain more than four items, typically three suggestions and a
/// delete button.
///
/// See also:
///
/// * [AdaptiveTextSelectionToolbar.buttonItems], the list of
/// [ContextMenuButtonItem]s that are used to build the buttons of the
/// text selection toolbar.
/// * [CupertinoSpellCheckSuggestionsToolbar.buttonItems], the list of
/// [ContextMenuButtonItem]s used to build the Cupertino style spell check
/// suggestions toolbar.
final List<ContextMenuButtonItem> buttonItems;
/// Builds the button items for the toolbar based on the available
/// spell check suggestions.
static List<ContextMenuButtonItem>? buildButtonItems(
EditableTextState editableTextState,
) {
// Determine if composing region is misspelled.
final SuggestionSpan? spanAtCursorIndex = editableTextState
.findSuggestionSpanAtCursorIndex(
editableTextState.currentTextEditingValue.selection.baseOffset,
);
if (spanAtCursorIndex == null) {
return null;
}
final List<ContextMenuButtonItem> buttonItems = <ContextMenuButtonItem>[];
// Build suggestion buttons.
for (final String suggestion in spanAtCursorIndex.suggestions.take(
_kMaxSuggestions,
)) {
buttonItems.add(
ContextMenuButtonItem(
onPressed: () {
if (!editableTextState.mounted) {
return;
}
_replaceText(
editableTextState,
suggestion,
spanAtCursorIndex.range,
);
},
label: suggestion,
),
);
}
// Build delete button.
final ContextMenuButtonItem deleteButton = ContextMenuButtonItem(
onPressed: () {
if (!editableTextState.mounted) {
return;
}
_replaceText(
editableTextState,
'',
editableTextState.currentTextEditingValue.composing,
);
},
type: ContextMenuButtonType.delete,
);
buttonItems.add(deleteButton);
return buttonItems;
}
static void _replaceText(
EditableTextState editableTextState,
String text,
TextRange replacementRange,
) {
// Replacement cannot be performed if the text is read only or obscured.
assert(
!editableTextState.widget.readOnly &&
!editableTextState.widget.obscureText,
);
final TextEditingValue newValue = editableTextState.textEditingValue
.replaced(
replacementRange,
text,
);
editableTextState.userUpdateTextEditingValue(
newValue,
SelectionChangedCause.toolbar,
);
// Schedule a call to bringIntoView() after renderEditable updates.
SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
if (editableTextState.mounted) {
editableTextState.bringIntoView(
editableTextState.textEditingValue.selection.extent,
);
}
}, debugLabel: 'SpellCheckerSuggestionsToolbar.bringIntoView');
editableTextState.hideToolbar();
}
/// Determines the Offset that the toolbar will be anchored to.
static Offset getToolbarAnchor(TextSelectionToolbarAnchors anchors) {
// Since this will be positioned below the anchor point, use the secondary
// anchor by default.
return anchors.secondaryAnchor == null
? anchors.primaryAnchor
: anchors.secondaryAnchor!;
}
/// Builds the toolbar buttons based on the [buttonItems].
List<Widget> _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,
),
);
if (buttonItem.type != ContextMenuButtonType.delete) {
return button;
}
return DecoratedBox(
decoration: const BoxDecoration(
border: Border(top: BorderSide(color: Colors.grey)),
),
child: button,
);
}).toList();
}
@override
Widget build(BuildContext context) {
if (buttonItems.isEmpty) {
return const SizedBox.shrink();
}
// Adjust toolbar height if needed.
final double spellCheckSuggestionsToolbarHeight =
_kDefaultToolbarHeight - (48.0 * (4 - buttonItems.length));
// Incorporate the padding distance between the content and toolbar.
final MediaQueryData mediaQueryData = MediaQuery.of(context);
final double softKeyboardViewInsetsBottom =
mediaQueryData.viewInsets.bottom;
final double paddingAbove =
mediaQueryData.padding.top +
CupertinoTextSelectionToolbar.kToolbarScreenPadding;
// Makes up for the Padding.
final Offset localAdjustment = Offset(
CupertinoTextSelectionToolbar.kToolbarScreenPadding,
paddingAbove,
);
return Padding(
padding: EdgeInsets.fromLTRB(
CupertinoTextSelectionToolbar.kToolbarScreenPadding,
paddingAbove,
CupertinoTextSelectionToolbar.kToolbarScreenPadding,
CupertinoTextSelectionToolbar.kToolbarScreenPadding +
softKeyboardViewInsetsBottom,
),
child: CustomSingleChildLayout(
delegate: SpellCheckSuggestionsToolbarLayoutDelegate(
anchor: anchor - localAdjustment,
),
child: AnimatedSize(
// This duration was eyeballed on a Pixel 2 emulator running Android
// API 28 for the Material TextSelectionToolbar.
duration: const Duration(milliseconds: 140),
child: _SpellCheckSuggestionsToolbarContainer(
height: spellCheckSuggestionsToolbarHeight,
children: <Widget>[..._buildToolbarButtons(context)],
),
),
),
);
}
}
/// The Material-styled toolbar outline for the spell check suggestions
/// toolbar.
class _SpellCheckSuggestionsToolbarContainer extends StatelessWidget {
const _SpellCheckSuggestionsToolbarContainer({
required this.height,
required this.children,
});
final double height;
final List<Widget> children;
@override
Widget build(BuildContext context) {
return Material(
// This elevation was eyeballed on a Pixel 4 emulator running Android
// API 31 for the SpellCheckSuggestionsToolbar.
elevation: 2.0,
type: MaterialType.card,
child: SizedBox(
// This width was eyeballed on a Pixel 4 emulator running Android
// API 31 for the SpellCheckSuggestionsToolbar.
width: 165.0,
height: height,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: children,
),
),
);
}
}

View File

@@ -0,0 +1,435 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/// @docImport 'package:flutter/material.dart';
library;
import 'package:PiliPlus/common/widgets/flutter/text_field/editable_text.dart';
import 'package:flutter/material.dart' hide EditableText, EditableTextState;
import 'package:flutter/services.dart';
/// Displays the system context menu on top of the Flutter view.
///
/// Currently, only supports iOS 16.0 and above and displays nothing on other
/// platforms.
///
/// The context menu is the menu that appears, for example, when doing text
/// selection. Flutter typically draws this menu itself, but this class deals
/// with the platform-rendered context menu instead.
///
/// There can only be one system context menu visible at a time. Building this
/// widget when the system context menu is already visible will hide the old one
/// and display this one. A system context menu that is hidden is informed via
/// [onSystemHide].
///
/// Pass [items] to specify the buttons that will appear in the menu. Any items
/// without a title will be given a default title from [WidgetsLocalizations].
///
/// By default, [items] will be set to the result of [getDefaultItems]. This
/// method considers the state of the [EditableTextState] so that, for example,
/// it will only include [IOSSystemContextMenuItemCopy] if there is currently a
/// selection to copy.
///
/// To check if the current device supports showing the system context menu,
/// call [isSupported].
///
/// {@tool dartpad}
/// This example shows how to create a [TextField] that uses the system context
/// menu where supported and does not show a system notification when the user
/// presses the "Paste" button.
///
/// ** See code in examples/api/lib/widgets/system_context_menu/system_context_menu.0.dart **
/// {@end-tool}
///
/// See also:
///
/// * [SystemContextMenuController], which directly controls the hiding and
/// showing of the system context menu.
class SystemContextMenu extends StatefulWidget {
/// Creates an instance of [SystemContextMenu] that points to the given
/// [anchor].
const SystemContextMenu._({
super.key,
required this.anchor,
required this.items,
this.onSystemHide,
});
/// Creates an instance of [SystemContextMenu] for the field indicated by the
/// given [EditableTextState].
factory SystemContextMenu.editableText({
Key? key,
required EditableTextState editableTextState,
List<IOSSystemContextMenuItem>? items,
}) {
final (
startGlyphHeight: double startGlyphHeight,
endGlyphHeight: double endGlyphHeight,
) = editableTextState
.getGlyphHeights();
return SystemContextMenu._(
key: key,
anchor: TextSelectionToolbarAnchors.getSelectionRect(
editableTextState.renderEditable,
startGlyphHeight,
endGlyphHeight,
editableTextState.renderEditable.getEndpointsForSelection(
editableTextState.textEditingValue.selection,
),
),
items: items ?? getDefaultItems(editableTextState),
onSystemHide: editableTextState.hideToolbar,
);
}
/// The [Rect] that the context menu should point to.
final Rect anchor;
/// A list of the items to be displayed in the system context menu.
///
/// When passed, items will be shown regardless of the state of text input.
/// For example, [IOSSystemContextMenuItemCopy] will produce a copy button
/// even when there is no selection to copy. Use [EditableTextState] and/or
/// the result of [getDefaultItems] to add and remove items based on the state
/// of the input.
///
/// Defaults to the result of [getDefaultItems].
final List<IOSSystemContextMenuItem> items;
/// Called when the system hides this context menu.
///
/// For example, tapping outside of the context menu typically causes the
/// system to hide the menu.
///
/// This is not called when showing a new system context menu causes another
/// to be hidden.
final VoidCallback? onSystemHide;
/// Whether the current device supports showing the system context menu.
///
/// Currently, this is only supported on newer versions of iOS.
static bool isSupported(BuildContext context) {
return MediaQuery.maybeSupportsShowingSystemContextMenu(context) ?? false;
}
/// The default [items] for the given [EditableTextState].
///
/// For example, [IOSSystemContextMenuItemCopy] will only be included when the
/// field represented by the [EditableTextState] has a selection.
///
/// See also:
///
/// * [EditableTextState.contextMenuButtonItems], which provides the default
/// [ContextMenuButtonItem]s for the Flutter-rendered context menu.
static List<IOSSystemContextMenuItem> getDefaultItems(
EditableTextState editableTextState,
) {
return <IOSSystemContextMenuItem>[
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(),
];
}
@override
State<SystemContextMenu> createState() => _SystemContextMenuState();
}
class _SystemContextMenuState extends State<SystemContextMenu> {
late final SystemContextMenuController _systemContextMenuController;
@override
void initState() {
super.initState();
_systemContextMenuController = SystemContextMenuController(
onSystemHide: widget.onSystemHide,
);
}
@override
void dispose() {
_systemContextMenuController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
assert(SystemContextMenu.isSupported(context));
if (widget.items.isNotEmpty) {
final WidgetsLocalizations localizations = WidgetsLocalizations.of(
context,
);
final List<IOSSystemContextMenuItemData> itemDatas = widget.items
.map((IOSSystemContextMenuItem item) => item.getData(localizations))
.toList();
_systemContextMenuController.showWithItems(widget.anchor, itemDatas);
}
return const SizedBox.shrink();
}
}
/// Describes a context menu button that will be rendered in the iOS system
/// context menu and not by Flutter itself.
///
/// See also:
///
/// * [SystemContextMenu], a widget that can be used to display the system
/// context menu.
/// * [IOSSystemContextMenuItemData], which performs a similar role but at the
/// method channel level and mirrors the requirements of the method channel
/// API.
/// * [ContextMenuButtonItem], which performs a similar role for Flutter-drawn
/// context menus.
@immutable
sealed class IOSSystemContextMenuItem {
const IOSSystemContextMenuItem();
/// The text to display to the user.
///
/// Not exposed for some built-in menu items whose title is always set by the
/// platform.
String? get title => null;
/// Returns the representation of this class used by method channels.
IOSSystemContextMenuItemData getData(WidgetsLocalizations localizations);
@override
int get hashCode => title.hashCode;
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (other.runtimeType != runtimeType) {
return false;
}
return other is IOSSystemContextMenuItem && other.title == title;
}
}
/// Creates an instance of [IOSSystemContextMenuItem] for the system's built-in
/// copy button.
///
/// Should only appear when there is a selection that can be copied.
///
/// The title and action are both handled by the platform.
///
/// See also:
///
/// * [SystemContextMenu], a widget that can be used to display the system
/// context menu.
/// * [IOSSystemContextMenuItemDataCopy], which specifies the data to be sent to
/// the platform for this same button.
final class IOSSystemContextMenuItemCopy extends IOSSystemContextMenuItem {
/// Creates an instance of [IOSSystemContextMenuItemCopy].
const IOSSystemContextMenuItemCopy();
@override
IOSSystemContextMenuItemDataCopy getData(WidgetsLocalizations localizations) {
return const IOSSystemContextMenuItemDataCopy();
}
}
/// Creates an instance of [IOSSystemContextMenuItem] for the system's built-in
/// cut button.
///
/// Should only appear when there is a selection that can be cut.
///
/// The title and action are both handled by the platform.
///
/// See also:
///
/// * [SystemContextMenu], a widget that can be used to display the system
/// context menu.
/// * [IOSSystemContextMenuItemDataCut], which specifies the data to be sent to
/// the platform for this same button.
final class IOSSystemContextMenuItemCut extends IOSSystemContextMenuItem {
/// Creates an instance of [IOSSystemContextMenuItemCut].
const IOSSystemContextMenuItemCut();
@override
IOSSystemContextMenuItemDataCut getData(WidgetsLocalizations localizations) {
return const IOSSystemContextMenuItemDataCut();
}
}
/// Creates an instance of [IOSSystemContextMenuItem] for the system's built-in
/// paste button.
///
/// Should only appear when the field can receive pasted content.
///
/// The title and action are both handled by the platform.
///
/// See also:
///
/// * [SystemContextMenu], a widget that can be used to display the system
/// context menu.
/// * [IOSSystemContextMenuItemDataPaste], which specifies the data to be sent
/// to the platform for this same button.
final class IOSSystemContextMenuItemPaste extends IOSSystemContextMenuItem {
/// Creates an instance of [IOSSystemContextMenuItemPaste].
const IOSSystemContextMenuItemPaste();
@override
IOSSystemContextMenuItemDataPaste getData(
WidgetsLocalizations localizations,
) {
return const IOSSystemContextMenuItemDataPaste();
}
}
/// Creates an instance of [IOSSystemContextMenuItem] for the system's built-in
/// select all button.
///
/// Should only appear when the field can have its selection changed.
///
/// The title and action are both handled by the platform.
///
/// See also:
///
/// * [SystemContextMenu], a widget that can be used to display the system
/// context menu.
/// * [IOSSystemContextMenuItemDataSelectAll], which specifies the data to be
/// sent to the platform for this same button.
final class IOSSystemContextMenuItemSelectAll extends IOSSystemContextMenuItem {
/// Creates an instance of [IOSSystemContextMenuItemSelectAll].
const IOSSystemContextMenuItemSelectAll();
@override
IOSSystemContextMenuItemDataSelectAll getData(
WidgetsLocalizations localizations,
) {
return const IOSSystemContextMenuItemDataSelectAll();
}
}
/// Creates an instance of [IOSSystemContextMenuItem] for the
/// system's built-in look up button.
///
/// Should only appear when content is selected.
///
/// The [title] is optional, but it must be specified before being sent to the
/// platform. Typically it should be set to
/// [WidgetsLocalizations.lookUpButtonLabel].
///
/// The action is handled by the platform.
///
/// See also:
///
/// * [SystemContextMenu], a widget that can be used to display the system
/// context menu.
/// * [IOSSystemContextMenuItemDataLookUp], which specifies the data to be sent
/// to the platform for this same button.
final class IOSSystemContextMenuItemLookUp extends IOSSystemContextMenuItem {
/// Creates an instance of [IOSSystemContextMenuItemLookUp].
const IOSSystemContextMenuItemLookUp({this.title});
@override
final String? title;
@override
IOSSystemContextMenuItemDataLookUp getData(
WidgetsLocalizations localizations,
) {
return IOSSystemContextMenuItemDataLookUp(
title: title ?? localizations.lookUpButtonLabel,
);
}
@override
String toString() {
return 'IOSSystemContextMenuItemLookUp(title: $title)';
}
}
/// Creates an instance of [IOSSystemContextMenuItem] for the
/// system's built-in search web button.
///
/// Should only appear when content is selected.
///
/// The [title] is optional, but it must be specified before being sent to the
/// platform. Typically it should be set to
/// [WidgetsLocalizations.searchWebButtonLabel].
///
/// The action is handled by the platform.
///
/// See also:
///
/// * [SystemContextMenu], a widget that can be used to display the system
/// context menu.
/// * [IOSSystemContextMenuItemDataSearchWeb], which specifies the data to be
/// sent to the platform for this same button.
final class IOSSystemContextMenuItemSearchWeb extends IOSSystemContextMenuItem {
/// Creates an instance of [IOSSystemContextMenuItemSearchWeb].
const IOSSystemContextMenuItemSearchWeb({this.title});
@override
final String? title;
@override
IOSSystemContextMenuItemDataSearchWeb getData(
WidgetsLocalizations localizations,
) {
return IOSSystemContextMenuItemDataSearchWeb(
title: title ?? localizations.searchWebButtonLabel,
);
}
@override
String toString() {
return 'IOSSystemContextMenuItemSearchWeb(title: $title)';
}
}
/// Creates an instance of [IOSSystemContextMenuItem] for the
/// system's built-in share button.
///
/// Opens the system share dialog.
///
/// Should only appear when shareable content is selected.
///
/// The [title] is optional, but it must be specified before being sent to the
/// platform. Typically it should be set to
/// [WidgetsLocalizations.shareButtonLabel].
///
/// See also:
///
/// * [SystemContextMenu], a widget that can be used to display the system
/// context menu.
/// * [IOSSystemContextMenuItemDataShare], which specifies the data to be sent
/// to the platform for this same button.
final class IOSSystemContextMenuItemShare extends IOSSystemContextMenuItem {
/// Creates an instance of [IOSSystemContextMenuItemShare].
const IOSSystemContextMenuItemShare({this.title});
@override
final String? title;
@override
IOSSystemContextMenuItemDataShare getData(
WidgetsLocalizations localizations,
) {
return IOSSystemContextMenuItemDataShare(
title: title ?? localizations.shareButtonLabel,
);
}
@override
String toString() {
return 'IOSSystemContextMenuItemShare(title: $title)';
}
}
// TODO(justinmc): Support the "custom" type.
// https://github.com/flutter/flutter/issues/103163

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff