mirror of
https://github.com/bggRGjQaUbCoE/PiliPlus.git
synced 2026-08-03 17:20:16 +08:00
File diff suppressed because it is too large
Load Diff
@@ -1,353 +0,0 @@
|
||||
// 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: prefer_initializing_formals
|
||||
|
||||
import 'dart:ui' as ui show TextHeightBehavior;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/flutter/text/paragraph.dart';
|
||||
import 'package:flutter/material.dart' hide RichText;
|
||||
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.
|
||||
///
|
||||
/// 
|
||||
///
|
||||
/// ```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.
|
||||
///
|
||||
/// 
|
||||
///
|
||||
/// ```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,
|
||||
required this.primary,
|
||||
this.onShowMore,
|
||||
}) : 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: text, primary: primary)
|
||||
..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
|
||||
..onShowMore = onShowMore;
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(
|
||||
EnumProperty<TextAlign>(
|
||||
'textAlign',
|
||||
textAlign,
|
||||
defaultValue: TextAlign.start,
|
||||
),
|
||||
)
|
||||
..add(
|
||||
EnumProperty<TextDirection>(
|
||||
'textDirection',
|
||||
textDirection,
|
||||
defaultValue: null,
|
||||
),
|
||||
)
|
||||
..add(
|
||||
FlagProperty(
|
||||
'softWrap',
|
||||
value: softWrap,
|
||||
ifTrue: 'wrapping at box width',
|
||||
ifFalse: 'no wrapping except at line break characters',
|
||||
showName: true,
|
||||
),
|
||||
)
|
||||
..add(
|
||||
EnumProperty<TextOverflow>(
|
||||
'overflow',
|
||||
overflow,
|
||||
defaultValue: TextOverflow.clip,
|
||||
),
|
||||
)
|
||||
..add(
|
||||
DiagnosticsProperty<TextScaler>(
|
||||
'textScaler',
|
||||
textScaler,
|
||||
defaultValue: TextScaler.noScaling,
|
||||
),
|
||||
)
|
||||
..add(IntProperty('maxLines', maxLines, ifNull: 'unlimited'))
|
||||
..add(
|
||||
EnumProperty<TextWidthBasis>(
|
||||
'textWidthBasis',
|
||||
textWidthBasis,
|
||||
defaultValue: TextWidthBasis.parent,
|
||||
),
|
||||
)
|
||||
..add(StringProperty('text', text.toPlainText()))
|
||||
..add(
|
||||
DiagnosticsProperty<Locale>('locale', locale, defaultValue: null),
|
||||
)
|
||||
..add(
|
||||
DiagnosticsProperty<StrutStyle>(
|
||||
'strutStyle',
|
||||
strutStyle,
|
||||
defaultValue: null,
|
||||
),
|
||||
)
|
||||
..add(
|
||||
DiagnosticsProperty<TextHeightBehavior>(
|
||||
'textHeightBehavior',
|
||||
textHeightBehavior,
|
||||
defaultValue: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -14,14 +14,10 @@
|
||||
/// @docImport 'widget_span.dart';
|
||||
library;
|
||||
|
||||
import 'dart:math';
|
||||
import 'dart:ui' as ui show TextHeightBehavior;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/flutter/text/paragraph.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text/rich_text.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart' hide Text, RichText;
|
||||
import 'package:flutter/rendering.dart' hide RenderParagraph;
|
||||
import 'package:PiliPlus/common/widgets/more_text/rich_text_more.dart';
|
||||
import 'package:flutter/material.dart' hide Text;
|
||||
|
||||
/// A run of text with a single style.
|
||||
///
|
||||
@@ -395,7 +391,7 @@ class Text extends StatelessWidget {
|
||||
);
|
||||
final double? wordSpacing = MediaQuery.maybeWordSpacingOverrideOf(context);
|
||||
final TextSpan effectiveTextSpan =
|
||||
_OverridingTextStyleTextSpanUtils.applyTextSpacingOverrides(
|
||||
OverridingTextStyleTextSpanUtils.applyTextSpacingOverrides(
|
||||
lineHeightScaleFactor: lineHeightScaleFactor,
|
||||
letterSpacing: letterSpacing,
|
||||
wordSpacing: wordSpacing,
|
||||
@@ -409,7 +405,6 @@ class Text extends StatelessWidget {
|
||||
final StrutStyle? effectiveStrutStyle = strutStyle?.merge(
|
||||
StrutStyle(height: lineHeightScaleFactor),
|
||||
);
|
||||
final SelectionRegistrar? registrar = SelectionContainer.maybeOf(context);
|
||||
final TextScaler textScaler = switch ((this.textScaler, textScaleFactor)) {
|
||||
(final TextScaler textScaler, _) => textScaler,
|
||||
// For unmigrated apps, fall back to textScaleFactor.
|
||||
@@ -418,68 +413,31 @@ class Text extends StatelessWidget {
|
||||
),
|
||||
(null, null) => MediaQuery.textScalerOf(context),
|
||||
};
|
||||
late Widget result;
|
||||
if (registrar != null) {
|
||||
result = MouseRegion(
|
||||
cursor:
|
||||
DefaultSelectionStyle.of(context).mouseCursor ??
|
||||
SystemMouseCursors.text,
|
||||
child: _SelectableTextContainer(
|
||||
textAlign: textAlign ?? defaultTextStyle.textAlign ?? TextAlign.start,
|
||||
textDirection:
|
||||
textDirection, // RichText uses Directionality.of to obtain a default if this is null.
|
||||
locale:
|
||||
locale, // RichText uses Localizations.localeOf to obtain a default if this is null
|
||||
softWrap: softWrap ?? defaultTextStyle.softWrap,
|
||||
overflow:
|
||||
overflow ??
|
||||
effectiveTextStyle?.overflow ??
|
||||
defaultTextStyle.overflow,
|
||||
textScaler: textScaler,
|
||||
maxLines: maxLines ?? defaultTextStyle.maxLines,
|
||||
strutStyle: effectiveStrutStyle,
|
||||
textWidthBasis: textWidthBasis ?? defaultTextStyle.textWidthBasis,
|
||||
textHeightBehavior:
|
||||
textHeightBehavior ??
|
||||
defaultTextStyle.textHeightBehavior ??
|
||||
DefaultTextHeightBehavior.maybeOf(context),
|
||||
selectionColor:
|
||||
selectionColor ??
|
||||
DefaultSelectionStyle.of(context).selectionColor ??
|
||||
DefaultSelectionStyle.defaultColor,
|
||||
text: effectiveTextSpan,
|
||||
primary: primary,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
result = RichText(
|
||||
textAlign: textAlign ?? defaultTextStyle.textAlign ?? TextAlign.start,
|
||||
textDirection:
|
||||
textDirection, // RichText uses Directionality.of to obtain a default if this is null.
|
||||
locale:
|
||||
locale, // RichText uses Localizations.localeOf to obtain a default if this is null
|
||||
softWrap: softWrap ?? defaultTextStyle.softWrap,
|
||||
overflow:
|
||||
overflow ??
|
||||
effectiveTextStyle?.overflow ??
|
||||
defaultTextStyle.overflow,
|
||||
textScaler: textScaler,
|
||||
maxLines: maxLines ?? defaultTextStyle.maxLines,
|
||||
strutStyle: effectiveStrutStyle,
|
||||
textWidthBasis: textWidthBasis ?? defaultTextStyle.textWidthBasis,
|
||||
textHeightBehavior:
|
||||
textHeightBehavior ??
|
||||
defaultTextStyle.textHeightBehavior ??
|
||||
DefaultTextHeightBehavior.maybeOf(context),
|
||||
selectionColor:
|
||||
selectionColor ??
|
||||
DefaultSelectionStyle.of(context).selectionColor ??
|
||||
DefaultSelectionStyle.defaultColor,
|
||||
text: effectiveTextSpan,
|
||||
primary: primary,
|
||||
onShowMore: onShowMore,
|
||||
);
|
||||
}
|
||||
Widget result = RichTextMore(
|
||||
textAlign: textAlign ?? defaultTextStyle.textAlign ?? TextAlign.start,
|
||||
textDirection:
|
||||
textDirection, // RichText uses Directionality.of to obtain a default if this is null.
|
||||
locale:
|
||||
locale, // RichText uses Localizations.localeOf to obtain a default if this is null
|
||||
softWrap: softWrap ?? defaultTextStyle.softWrap,
|
||||
overflow:
|
||||
overflow ?? effectiveTextStyle?.overflow ?? defaultTextStyle.overflow,
|
||||
textScaler: textScaler,
|
||||
maxLines: maxLines ?? defaultTextStyle.maxLines,
|
||||
strutStyle: effectiveStrutStyle,
|
||||
textWidthBasis: textWidthBasis ?? defaultTextStyle.textWidthBasis,
|
||||
textHeightBehavior:
|
||||
textHeightBehavior ??
|
||||
defaultTextStyle.textHeightBehavior ??
|
||||
DefaultTextHeightBehavior.maybeOf(context),
|
||||
selectionColor:
|
||||
selectionColor ??
|
||||
DefaultSelectionStyle.of(context).selectionColor ??
|
||||
DefaultSelectionStyle.defaultColor,
|
||||
text: effectiveTextSpan,
|
||||
primary: primary,
|
||||
onShowMore: onShowMore,
|
||||
);
|
||||
if (semanticsLabel != null || semanticsIdentifier != null) {
|
||||
result = Semantics(
|
||||
textDirection: textDirection,
|
||||
@@ -493,853 +451,4 @@ class Text extends StatelessWidget {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(StringProperty('data', data, showName: false));
|
||||
if (textSpan != null) {
|
||||
properties.add(
|
||||
textSpan!.toDiagnosticsNode(
|
||||
name: 'textSpan',
|
||||
style: DiagnosticsTreeStyle.transition,
|
||||
),
|
||||
);
|
||||
}
|
||||
style?.debugFillProperties(properties);
|
||||
properties
|
||||
..add(
|
||||
EnumProperty<TextAlign>('textAlign', textAlign, defaultValue: null),
|
||||
)
|
||||
..add(
|
||||
EnumProperty<TextDirection>(
|
||||
'textDirection',
|
||||
textDirection,
|
||||
defaultValue: null,
|
||||
),
|
||||
)
|
||||
..add(
|
||||
DiagnosticsProperty<Locale>('locale', locale, defaultValue: null),
|
||||
)
|
||||
..add(
|
||||
FlagProperty(
|
||||
'softWrap',
|
||||
value: softWrap,
|
||||
ifTrue: 'wrapping at box width',
|
||||
ifFalse: 'no wrapping except at line break characters',
|
||||
showName: true,
|
||||
),
|
||||
)
|
||||
..add(
|
||||
EnumProperty<TextOverflow>('overflow', overflow, defaultValue: null),
|
||||
)
|
||||
..add(
|
||||
DoubleProperty('textScaleFactor', textScaleFactor, defaultValue: null),
|
||||
)
|
||||
..add(IntProperty('maxLines', maxLines, defaultValue: null))
|
||||
..add(
|
||||
EnumProperty<TextWidthBasis>(
|
||||
'textWidthBasis',
|
||||
textWidthBasis,
|
||||
defaultValue: null,
|
||||
),
|
||||
)
|
||||
..add(
|
||||
DiagnosticsProperty<ui.TextHeightBehavior>(
|
||||
'textHeightBehavior',
|
||||
textHeightBehavior,
|
||||
defaultValue: null,
|
||||
),
|
||||
);
|
||||
if (semanticsLabel != null) {
|
||||
properties.add(StringProperty('semanticsLabel', semanticsLabel));
|
||||
}
|
||||
if (semanticsIdentifier != null) {
|
||||
properties.add(
|
||||
StringProperty('semanticsIdentifier', semanticsIdentifier),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _SelectableTextContainer extends StatefulWidget {
|
||||
const _SelectableTextContainer({
|
||||
required this.text,
|
||||
required this.textAlign,
|
||||
this.textDirection,
|
||||
required this.softWrap,
|
||||
required this.overflow,
|
||||
required this.textScaler,
|
||||
this.maxLines,
|
||||
this.locale,
|
||||
this.strutStyle,
|
||||
required this.textWidthBasis,
|
||||
this.textHeightBehavior,
|
||||
required this.selectionColor,
|
||||
required this.primary,
|
||||
});
|
||||
|
||||
final TextSpan text;
|
||||
final TextAlign textAlign;
|
||||
final TextDirection? textDirection;
|
||||
final bool softWrap;
|
||||
final TextOverflow overflow;
|
||||
final TextScaler textScaler;
|
||||
final int? maxLines;
|
||||
final Locale? locale;
|
||||
final StrutStyle? strutStyle;
|
||||
final TextWidthBasis textWidthBasis;
|
||||
final ui.TextHeightBehavior? textHeightBehavior;
|
||||
final Color selectionColor;
|
||||
final Color primary;
|
||||
|
||||
@override
|
||||
State<_SelectableTextContainer> createState() =>
|
||||
_SelectableTextContainerState();
|
||||
}
|
||||
|
||||
class _SelectableTextContainerState extends State<_SelectableTextContainer> {
|
||||
late final _SelectableTextContainerDelegate _selectionDelegate;
|
||||
final GlobalKey _textKey = GlobalKey();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_selectionDelegate = _SelectableTextContainerDelegate(_textKey);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_selectionDelegate.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SelectionContainer(
|
||||
delegate: _selectionDelegate,
|
||||
// Use [_RichText] wrapper so the underlying [RenderParagraph] can register
|
||||
// its [Selectable]s to the [SelectionContainer] created by this widget.
|
||||
child: _RichText(
|
||||
textKey: _textKey,
|
||||
textAlign: widget.textAlign,
|
||||
textDirection: widget.textDirection,
|
||||
locale: widget.locale,
|
||||
softWrap: widget.softWrap,
|
||||
overflow: widget.overflow,
|
||||
textScaler: widget.textScaler,
|
||||
maxLines: widget.maxLines,
|
||||
strutStyle: widget.strutStyle,
|
||||
textWidthBasis: widget.textWidthBasis,
|
||||
textHeightBehavior: widget.textHeightBehavior,
|
||||
selectionColor: widget.selectionColor,
|
||||
text: widget.text,
|
||||
primary: widget.primary,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RichText extends StatelessWidget {
|
||||
const _RichText({
|
||||
this.textKey,
|
||||
required this.text,
|
||||
required this.textAlign,
|
||||
this.textDirection,
|
||||
required this.softWrap,
|
||||
required this.overflow,
|
||||
required this.textScaler,
|
||||
this.maxLines,
|
||||
this.locale,
|
||||
this.strutStyle,
|
||||
required this.textWidthBasis,
|
||||
this.textHeightBehavior,
|
||||
required this.selectionColor,
|
||||
required this.primary,
|
||||
});
|
||||
|
||||
final GlobalKey? textKey;
|
||||
final InlineSpan text;
|
||||
final TextAlign textAlign;
|
||||
final TextDirection? textDirection;
|
||||
final bool softWrap;
|
||||
final TextOverflow overflow;
|
||||
final TextScaler textScaler;
|
||||
final int? maxLines;
|
||||
final Locale? locale;
|
||||
final StrutStyle? strutStyle;
|
||||
final TextWidthBasis textWidthBasis;
|
||||
final ui.TextHeightBehavior? textHeightBehavior;
|
||||
final Color selectionColor;
|
||||
final Color primary;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final SelectionRegistrar? registrar = SelectionContainer.maybeOf(context);
|
||||
return RichText(
|
||||
key: textKey,
|
||||
textAlign: textAlign,
|
||||
textDirection: textDirection,
|
||||
locale: locale,
|
||||
softWrap: softWrap,
|
||||
overflow: overflow,
|
||||
textScaler: textScaler,
|
||||
maxLines: maxLines,
|
||||
strutStyle: strutStyle,
|
||||
textWidthBasis: textWidthBasis,
|
||||
textHeightBehavior: textHeightBehavior,
|
||||
selectionRegistrar: registrar,
|
||||
selectionColor: selectionColor,
|
||||
text: text,
|
||||
primary: primary,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// In practice some selectables like widgetspan shift several pixels. So when
|
||||
// the vertical position diff is within the threshold, compare the horizontal
|
||||
// position to make the compareScreenOrder function more robust.
|
||||
const double _kSelectableVerticalComparingThreshold = 3.0;
|
||||
|
||||
class _SelectableTextContainerDelegate
|
||||
extends StaticSelectionContainerDelegate {
|
||||
_SelectableTextContainerDelegate(GlobalKey textKey) : _textKey = textKey;
|
||||
|
||||
final GlobalKey _textKey;
|
||||
RenderParagraph get paragraph =>
|
||||
_textKey.currentContext!.findRenderObject()! as RenderParagraph;
|
||||
|
||||
@override
|
||||
SelectionResult handleSelectParagraph(SelectParagraphSelectionEvent event) {
|
||||
final SelectionResult result = _handleSelectParagraph(event);
|
||||
super.didReceiveSelectionBoundaryEvents();
|
||||
return result;
|
||||
}
|
||||
|
||||
SelectionResult _handleSelectParagraph(SelectParagraphSelectionEvent event) {
|
||||
if (event.absorb) {
|
||||
for (var index = 0; index < selectables.length; index += 1) {
|
||||
dispatchSelectionEventToChild(selectables[index], event);
|
||||
}
|
||||
currentSelectionStartIndex = 0;
|
||||
currentSelectionEndIndex = selectables.length - 1;
|
||||
return SelectionResult.next;
|
||||
}
|
||||
|
||||
// First pass, if the position is on a placeholder then dispatch the selection
|
||||
// event to the [Selectable] at the location and terminate.
|
||||
for (var index = 0; index < selectables.length; index += 1) {
|
||||
final bool selectableIsPlaceholder = !paragraph
|
||||
.selectableBelongsToParagraph(selectables[index]);
|
||||
if (selectableIsPlaceholder &&
|
||||
selectables[index].boundingBoxes.isNotEmpty) {
|
||||
for (final Rect rect in selectables[index].boundingBoxes) {
|
||||
final Rect globalRect = MatrixUtils.transformRect(
|
||||
selectables[index].getTransformTo(null),
|
||||
rect,
|
||||
);
|
||||
if (globalRect.contains(event.globalPosition)) {
|
||||
currentSelectionStartIndex = currentSelectionEndIndex = index;
|
||||
return dispatchSelectionEventToChild(selectables[index], event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SelectionResult? lastSelectionResult;
|
||||
var foundStart = false;
|
||||
int? lastNextIndex;
|
||||
for (var index = 0; index < selectables.length; index += 1) {
|
||||
if (!paragraph.selectableBelongsToParagraph(selectables[index])) {
|
||||
if (foundStart) {
|
||||
final SelectionEvent synthesizedEvent = SelectParagraphSelectionEvent(
|
||||
globalPosition: event.globalPosition,
|
||||
absorb: true,
|
||||
);
|
||||
final SelectionResult result = dispatchSelectionEventToChild(
|
||||
selectables[index],
|
||||
synthesizedEvent,
|
||||
);
|
||||
if (selectables.length - 1 == index) {
|
||||
currentSelectionEndIndex = index;
|
||||
_flushInactiveSelections();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
final SelectionGeometry existingGeometry = selectables[index].value;
|
||||
lastSelectionResult = dispatchSelectionEventToChild(
|
||||
selectables[index],
|
||||
event,
|
||||
);
|
||||
if (index == selectables.length - 1 &&
|
||||
lastSelectionResult == SelectionResult.next) {
|
||||
if (foundStart) {
|
||||
currentSelectionEndIndex = index;
|
||||
} else {
|
||||
currentSelectionStartIndex = currentSelectionEndIndex = index;
|
||||
}
|
||||
return SelectionResult.next;
|
||||
}
|
||||
if (lastSelectionResult == SelectionResult.next) {
|
||||
if (selectables[index].value == existingGeometry && !foundStart) {
|
||||
lastNextIndex = index;
|
||||
}
|
||||
if (selectables[index].value != existingGeometry && !foundStart) {
|
||||
assert(selectables[index].boundingBoxes.isNotEmpty);
|
||||
assert(selectables[index].value.selectionRects.isNotEmpty);
|
||||
final bool selectionAtStartOfSelectable = selectables[index]
|
||||
.boundingBoxes[0]
|
||||
.overlaps(
|
||||
selectables[index].value.selectionRects[0],
|
||||
);
|
||||
var startIndex = 0;
|
||||
if (lastNextIndex != null && selectionAtStartOfSelectable) {
|
||||
startIndex = lastNextIndex + 1;
|
||||
} else {
|
||||
startIndex = lastNextIndex == null && selectionAtStartOfSelectable
|
||||
? 0
|
||||
: index;
|
||||
}
|
||||
for (var i = startIndex; i < index; i += 1) {
|
||||
final SelectionEvent synthesizedEvent =
|
||||
SelectParagraphSelectionEvent(
|
||||
globalPosition: event.globalPosition,
|
||||
absorb: true,
|
||||
);
|
||||
dispatchSelectionEventToChild(selectables[i], synthesizedEvent);
|
||||
}
|
||||
currentSelectionStartIndex = startIndex;
|
||||
foundStart = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (index == 0 && lastSelectionResult == SelectionResult.previous) {
|
||||
return SelectionResult.previous;
|
||||
}
|
||||
if (selectables[index].value != existingGeometry) {
|
||||
if (!foundStart && lastNextIndex == null) {
|
||||
currentSelectionStartIndex = 0;
|
||||
for (var i = 0; i < index; i += 1) {
|
||||
final SelectionEvent synthesizedEvent =
|
||||
SelectParagraphSelectionEvent(
|
||||
globalPosition: event.globalPosition,
|
||||
absorb: true,
|
||||
);
|
||||
dispatchSelectionEventToChild(selectables[i], synthesizedEvent);
|
||||
}
|
||||
}
|
||||
currentSelectionEndIndex = index;
|
||||
// Geometry has changed as a result of select paragraph, need to clear the
|
||||
// selection of other selectables to keep selection in sync.
|
||||
_flushInactiveSelections();
|
||||
}
|
||||
return SelectionResult.end;
|
||||
}
|
||||
assert(lastSelectionResult == null);
|
||||
return SelectionResult.end;
|
||||
}
|
||||
|
||||
/// Initializes the selection of the selectable children.
|
||||
///
|
||||
/// The goal is to find the selectable child that contains the selection edge.
|
||||
/// Returns [SelectionResult.end] if the selection edge ends on any of the
|
||||
/// children. Otherwise, it returns [SelectionResult.previous] if the selection
|
||||
/// does not reach any of its children. Returns [SelectionResult.next]
|
||||
/// if the selection reaches the end of its children.
|
||||
///
|
||||
/// Ideally, this method should only be called twice at the beginning of the
|
||||
/// drag selection, once for start edge update event, once for end edge update
|
||||
/// event.
|
||||
SelectionResult _initSelection(
|
||||
SelectionEdgeUpdateEvent event, {
|
||||
required bool isEnd,
|
||||
}) {
|
||||
assert(
|
||||
(isEnd && currentSelectionEndIndex == -1) ||
|
||||
(!isEnd && currentSelectionStartIndex == -1),
|
||||
);
|
||||
SelectionResult? finalResult;
|
||||
// Begin the search for the selection edge at the opposite edge if it exists.
|
||||
final hasOppositeEdge = isEnd
|
||||
? currentSelectionStartIndex != -1
|
||||
: currentSelectionEndIndex != -1;
|
||||
int newIndex = switch ((isEnd, hasOppositeEdge)) {
|
||||
(true, true) => currentSelectionStartIndex,
|
||||
(true, false) => 0,
|
||||
(false, true) => currentSelectionEndIndex,
|
||||
(false, false) => 0,
|
||||
};
|
||||
bool? forward;
|
||||
late SelectionResult currentSelectableResult;
|
||||
// This loop sends the selection event to one of the following to determine
|
||||
// the direction of the search.
|
||||
// - The opposite edge index if it exists.
|
||||
// - Index 0 if the opposite edge index does not exist.
|
||||
//
|
||||
// If the result is `SelectionResult.next`, this loop look backward.
|
||||
// Otherwise, it looks forward.
|
||||
//
|
||||
// The terminate condition are:
|
||||
// 1. the selectable returns end, pending, none.
|
||||
// 2. the selectable returns previous when looking forward.
|
||||
// 2. the selectable returns next when looking backward.
|
||||
while (newIndex < selectables.length &&
|
||||
newIndex >= 0 &&
|
||||
finalResult == null) {
|
||||
currentSelectableResult = dispatchSelectionEventToChild(
|
||||
selectables[newIndex],
|
||||
event,
|
||||
);
|
||||
switch (currentSelectableResult) {
|
||||
case SelectionResult.end:
|
||||
case SelectionResult.pending:
|
||||
case SelectionResult.none:
|
||||
finalResult = currentSelectableResult;
|
||||
case SelectionResult.next:
|
||||
if (forward == false) {
|
||||
newIndex += 1;
|
||||
finalResult = SelectionResult.end;
|
||||
} else if (newIndex == selectables.length - 1) {
|
||||
finalResult = currentSelectableResult;
|
||||
} else {
|
||||
forward = true;
|
||||
newIndex += 1;
|
||||
}
|
||||
case SelectionResult.previous:
|
||||
if (forward ?? false) {
|
||||
newIndex -= 1;
|
||||
finalResult = SelectionResult.end;
|
||||
} else if (newIndex == 0) {
|
||||
finalResult = currentSelectableResult;
|
||||
} else {
|
||||
forward = false;
|
||||
newIndex -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isEnd) {
|
||||
currentSelectionEndIndex = newIndex;
|
||||
} else {
|
||||
currentSelectionStartIndex = newIndex;
|
||||
}
|
||||
_flushInactiveSelections();
|
||||
return finalResult!;
|
||||
}
|
||||
|
||||
SelectionResult _adjustSelection(
|
||||
SelectionEdgeUpdateEvent event, {
|
||||
required bool isEnd,
|
||||
}) {
|
||||
assert(() {
|
||||
if (isEnd) {
|
||||
assert(
|
||||
currentSelectionEndIndex < selectables.length &&
|
||||
currentSelectionEndIndex >= 0,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
assert(
|
||||
currentSelectionStartIndex < selectables.length &&
|
||||
currentSelectionStartIndex >= 0,
|
||||
);
|
||||
return true;
|
||||
}());
|
||||
SelectionResult? finalResult;
|
||||
// Determines if the edge being adjusted is within the current viewport.
|
||||
// - If so, we begin the search for the new selection edge position at the
|
||||
// currentSelectionEndIndex/currentSelectionStartIndex.
|
||||
// - If not, we attempt to locate the new selection edge starting from
|
||||
// the opposite end.
|
||||
// - If neither edge is in the current viewport, the search for the new
|
||||
// selection edge position begins at 0.
|
||||
//
|
||||
// This can happen when there is a scrollable child and the edge being adjusted
|
||||
// has been scrolled out of view.
|
||||
final isCurrentEdgeWithinViewport = isEnd
|
||||
? value.endSelectionPoint != null
|
||||
: value.startSelectionPoint != null;
|
||||
final isOppositeEdgeWithinViewport = isEnd
|
||||
? value.startSelectionPoint != null
|
||||
: value.endSelectionPoint != null;
|
||||
int newIndex = switch ((
|
||||
isEnd,
|
||||
isCurrentEdgeWithinViewport,
|
||||
isOppositeEdgeWithinViewport,
|
||||
)) {
|
||||
(true, true, true) => currentSelectionEndIndex,
|
||||
(true, true, false) => currentSelectionEndIndex,
|
||||
(true, false, true) => currentSelectionStartIndex,
|
||||
(true, false, false) => 0,
|
||||
(false, true, true) => currentSelectionStartIndex,
|
||||
(false, true, false) => currentSelectionStartIndex,
|
||||
(false, false, true) => currentSelectionEndIndex,
|
||||
(false, false, false) => 0,
|
||||
};
|
||||
bool? forward;
|
||||
late SelectionResult currentSelectableResult;
|
||||
// This loop sends the selection event to one of the following to determine
|
||||
// the direction of the search.
|
||||
// - currentSelectionEndIndex/currentSelectionStartIndex if the current edge
|
||||
// is in the current viewport.
|
||||
// - The opposite edge index if the current edge is not in the current viewport.
|
||||
// - Index 0 if neither edge is in the current viewport.
|
||||
//
|
||||
// If the result is `SelectionResult.next`, this loop look backward.
|
||||
// Otherwise, it looks forward.
|
||||
//
|
||||
// The terminate condition are:
|
||||
// 1. the selectable returns end, pending, none.
|
||||
// 2. the selectable returns previous when looking forward.
|
||||
// 2. the selectable returns next when looking backward.
|
||||
while (newIndex < selectables.length &&
|
||||
newIndex >= 0 &&
|
||||
finalResult == null) {
|
||||
currentSelectableResult = dispatchSelectionEventToChild(
|
||||
selectables[newIndex],
|
||||
event,
|
||||
);
|
||||
switch (currentSelectableResult) {
|
||||
case SelectionResult.end:
|
||||
case SelectionResult.pending:
|
||||
case SelectionResult.none:
|
||||
finalResult = currentSelectableResult;
|
||||
case SelectionResult.next:
|
||||
if (forward == false) {
|
||||
newIndex += 1;
|
||||
finalResult = SelectionResult.end;
|
||||
} else if (newIndex == selectables.length - 1) {
|
||||
finalResult = currentSelectableResult;
|
||||
} else {
|
||||
forward = true;
|
||||
newIndex += 1;
|
||||
}
|
||||
case SelectionResult.previous:
|
||||
if (forward ?? false) {
|
||||
newIndex -= 1;
|
||||
finalResult = SelectionResult.end;
|
||||
} else if (newIndex == 0) {
|
||||
finalResult = currentSelectableResult;
|
||||
} else {
|
||||
forward = false;
|
||||
newIndex -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isEnd) {
|
||||
final bool forwardSelection =
|
||||
currentSelectionEndIndex >= currentSelectionStartIndex;
|
||||
if (forward != null &&
|
||||
((!forwardSelection &&
|
||||
forward &&
|
||||
newIndex >= currentSelectionStartIndex) ||
|
||||
(forwardSelection &&
|
||||
!forward &&
|
||||
newIndex <= currentSelectionStartIndex))) {
|
||||
currentSelectionStartIndex = currentSelectionEndIndex;
|
||||
}
|
||||
currentSelectionEndIndex = newIndex;
|
||||
} else {
|
||||
final bool forwardSelection =
|
||||
currentSelectionEndIndex >= currentSelectionStartIndex;
|
||||
if (forward != null &&
|
||||
((!forwardSelection &&
|
||||
!forward &&
|
||||
newIndex <= currentSelectionEndIndex) ||
|
||||
(forwardSelection &&
|
||||
forward &&
|
||||
newIndex >= currentSelectionEndIndex))) {
|
||||
currentSelectionEndIndex = currentSelectionStartIndex;
|
||||
}
|
||||
currentSelectionStartIndex = newIndex;
|
||||
}
|
||||
_flushInactiveSelections();
|
||||
return finalResult!;
|
||||
}
|
||||
|
||||
/// The compare function this delegate used for determining the selection
|
||||
/// order of the [Selectable]s.
|
||||
///
|
||||
/// Sorts the [Selectable]s by their top left [Rect].
|
||||
@override
|
||||
Comparator<Selectable> get compareOrder => _compareScreenOrder;
|
||||
|
||||
static int _compareScreenOrder(Selectable a, Selectable b) {
|
||||
// Attempt to sort the selectables under a [_SelectableTextContainerDelegate]
|
||||
// by the top left rect.
|
||||
final Rect rectA = MatrixUtils.transformRect(
|
||||
a.getTransformTo(null),
|
||||
a.boundingBoxes.first,
|
||||
);
|
||||
final Rect rectB = MatrixUtils.transformRect(
|
||||
b.getTransformTo(null),
|
||||
b.boundingBoxes.first,
|
||||
);
|
||||
final int result = _compareVertically(rectA, rectB);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
return _compareHorizontally(rectA, rectB);
|
||||
}
|
||||
|
||||
/// Compares two rectangles in the screen order solely by their vertical
|
||||
/// positions.
|
||||
///
|
||||
/// Returns positive if a is lower, negative if a is higher, 0 if their
|
||||
/// order can't be determine solely by their vertical position.
|
||||
static int _compareVertically(Rect a, Rect b) {
|
||||
// The rectangles overlap so defer to horizontal comparison.
|
||||
if ((a.top - b.top < _kSelectableVerticalComparingThreshold &&
|
||||
a.bottom - b.bottom > -_kSelectableVerticalComparingThreshold) ||
|
||||
(b.top - a.top < _kSelectableVerticalComparingThreshold &&
|
||||
b.bottom - a.bottom > -_kSelectableVerticalComparingThreshold)) {
|
||||
return 0;
|
||||
}
|
||||
if ((a.top - b.top).abs() > _kSelectableVerticalComparingThreshold) {
|
||||
return a.top > b.top ? 1 : -1;
|
||||
}
|
||||
return a.bottom > b.bottom ? 1 : -1;
|
||||
}
|
||||
|
||||
/// Compares two rectangles in the screen order by their horizontal positions
|
||||
/// assuming one of the rectangles enclose the other rect vertically.
|
||||
///
|
||||
/// Returns positive if a is lower, negative if a is higher.
|
||||
static int _compareHorizontally(Rect a, Rect b) {
|
||||
// a encloses b.
|
||||
if (a.left - b.left < precisionErrorTolerance &&
|
||||
a.right - b.right > -precisionErrorTolerance) {
|
||||
return -1;
|
||||
}
|
||||
// b encloses a.
|
||||
if (b.left - a.left < precisionErrorTolerance &&
|
||||
b.right - a.right > -precisionErrorTolerance) {
|
||||
return 1;
|
||||
}
|
||||
if ((a.left - b.left).abs() > precisionErrorTolerance) {
|
||||
return a.left > b.left ? 1 : -1;
|
||||
}
|
||||
return a.right > b.right ? 1 : -1;
|
||||
}
|
||||
|
||||
/// This method calculates a local [SelectedContentRange] based on the list
|
||||
/// of [selections] that are accumulated from the [Selectable] children under this
|
||||
/// delegate. This calculation takes into account the accumulated content
|
||||
/// length before the active selection, and returns null when either selection
|
||||
/// edge has not been set.
|
||||
SelectedContentRange? _calculateLocalRange(List<_SelectionInfo> selections) {
|
||||
if (currentSelectionStartIndex == -1 || currentSelectionEndIndex == -1) {
|
||||
return null;
|
||||
}
|
||||
var startOffset = 0;
|
||||
var endOffset = 0;
|
||||
var foundStart = false;
|
||||
bool forwardSelection =
|
||||
currentSelectionEndIndex >= currentSelectionStartIndex;
|
||||
if (currentSelectionEndIndex == currentSelectionStartIndex) {
|
||||
// Determining selection direction is inaccurate if currentSelectionStartIndex == currentSelectionEndIndex.
|
||||
// Use the range from the selectable within the selection as the source of truth for selection direction.
|
||||
final SelectedContentRange rangeAtSelectableInSelection =
|
||||
selectables[currentSelectionStartIndex].getSelection()!;
|
||||
forwardSelection =
|
||||
rangeAtSelectableInSelection.endOffset >=
|
||||
rangeAtSelectableInSelection.startOffset;
|
||||
}
|
||||
for (var index = 0; index < selections.length; index++) {
|
||||
final _SelectionInfo selection = selections[index];
|
||||
if (selection.range == null) {
|
||||
if (foundStart) {
|
||||
return SelectedContentRange(
|
||||
startOffset: forwardSelection ? startOffset : endOffset,
|
||||
endOffset: forwardSelection ? endOffset : startOffset,
|
||||
);
|
||||
}
|
||||
startOffset += selection.contentLength;
|
||||
endOffset = startOffset;
|
||||
continue;
|
||||
}
|
||||
final int selectionStartNormalized = min(
|
||||
selection.range!.startOffset,
|
||||
selection.range!.endOffset,
|
||||
);
|
||||
final int selectionEndNormalized = max(
|
||||
selection.range!.startOffset,
|
||||
selection.range!.endOffset,
|
||||
);
|
||||
if (!foundStart) {
|
||||
// Because a RenderParagraph may split its content into multiple selectables
|
||||
// we have to consider at what offset a selectable starts at relative
|
||||
// to the RenderParagraph, when the selectable is not the start of the content.
|
||||
final bool shouldConsiderContentStart =
|
||||
index > 0 &&
|
||||
paragraph.selectableBelongsToParagraph(selectables[index]);
|
||||
startOffset +=
|
||||
(selectionStartNormalized -
|
||||
(shouldConsiderContentStart
|
||||
? paragraph
|
||||
.getPositionForOffset(
|
||||
selectables[index]
|
||||
.boundingBoxes
|
||||
.first
|
||||
.centerLeft,
|
||||
)
|
||||
.offset
|
||||
: 0))
|
||||
.abs();
|
||||
endOffset =
|
||||
startOffset +
|
||||
(selectionEndNormalized - selectionStartNormalized).abs();
|
||||
foundStart = true;
|
||||
} else {
|
||||
endOffset += (selectionEndNormalized - selectionStartNormalized).abs();
|
||||
}
|
||||
}
|
||||
assert(
|
||||
foundStart,
|
||||
'The start of the selection has not been found despite this selection delegate having an existing currentSelectionStartIndex and currentSelectionEndIndex.',
|
||||
);
|
||||
return SelectedContentRange(
|
||||
startOffset: forwardSelection ? startOffset : endOffset,
|
||||
endOffset: forwardSelection ? endOffset : startOffset,
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns a [SelectedContentRange] considering the [SelectedContentRange]
|
||||
/// from each [Selectable] child managed under this delegate.
|
||||
///
|
||||
/// When nothing is selected or either selection edge has not been set,
|
||||
/// this method will return `null`.
|
||||
@override
|
||||
SelectedContentRange? getSelection() {
|
||||
final selections = <_SelectionInfo>[
|
||||
for (final Selectable selectable in selectables)
|
||||
(
|
||||
contentLength: selectable.contentLength,
|
||||
range: selectable.getSelection(),
|
||||
),
|
||||
];
|
||||
return _calculateLocalRange(selections);
|
||||
}
|
||||
|
||||
// From [SelectableRegion].
|
||||
|
||||
// Clears the selection on all selectables not in the range of
|
||||
// currentSelectionStartIndex..currentSelectionEndIndex.
|
||||
//
|
||||
// If one of the edges does not exist, then this method will clear the selection
|
||||
// in all selectables except the existing edge.
|
||||
//
|
||||
// If neither of the edges exist this method immediately returns.
|
||||
void _flushInactiveSelections() {
|
||||
if (currentSelectionStartIndex == -1 && currentSelectionEndIndex == -1) {
|
||||
return;
|
||||
}
|
||||
if (currentSelectionStartIndex == -1 || currentSelectionEndIndex == -1) {
|
||||
final int skipIndex = currentSelectionStartIndex == -1
|
||||
? currentSelectionEndIndex
|
||||
: currentSelectionStartIndex;
|
||||
selectables
|
||||
.where((Selectable target) => target != selectables[skipIndex])
|
||||
.forEach(
|
||||
(Selectable target) => dispatchSelectionEventToChild(
|
||||
target,
|
||||
const ClearSelectionEvent(),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
final int skipStart = min(
|
||||
currentSelectionStartIndex,
|
||||
currentSelectionEndIndex,
|
||||
);
|
||||
final int skipEnd = max(
|
||||
currentSelectionStartIndex,
|
||||
currentSelectionEndIndex,
|
||||
);
|
||||
for (var index = 0; index < selectables.length; index += 1) {
|
||||
if (index >= skipStart && index <= skipEnd) {
|
||||
continue;
|
||||
}
|
||||
dispatchSelectionEventToChild(
|
||||
selectables[index],
|
||||
const ClearSelectionEvent(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
SelectionResult handleSelectionEdgeUpdate(SelectionEdgeUpdateEvent event) {
|
||||
if (event.granularity != TextGranularity.paragraph) {
|
||||
return super.handleSelectionEdgeUpdate(event);
|
||||
}
|
||||
updateLastSelectionEdgeLocation(
|
||||
globalSelectionEdgeLocation: event.globalPosition,
|
||||
forEnd: event.type == SelectionEventType.endEdgeUpdate,
|
||||
);
|
||||
if (event.type == SelectionEventType.endEdgeUpdate) {
|
||||
return currentSelectionEndIndex == -1
|
||||
? _initSelection(event, isEnd: true)
|
||||
: _adjustSelection(event, isEnd: true);
|
||||
}
|
||||
return currentSelectionStartIndex == -1
|
||||
? _initSelection(event, isEnd: false)
|
||||
: _adjustSelection(event, isEnd: false);
|
||||
}
|
||||
}
|
||||
|
||||
/// The length of the content that can be selected, and the range that is
|
||||
/// selected.
|
||||
typedef _SelectionInfo = ({int contentLength, SelectedContentRange? range});
|
||||
|
||||
/// A utility class for overriding the text styles of a [TextSpan] tree.
|
||||
// When changes are made to this class, the equivalent API in editable_text.dart
|
||||
// must also be updated.
|
||||
// TODO(Renzo-Olivares): Remove after investigating a solution for overriding all
|
||||
// styles for children in an [InlineSpan] tree, see: https://github.com/flutter/flutter/issues/177952.
|
||||
class _OverridingTextStyleTextSpanUtils {
|
||||
static TextSpan applyTextSpacingOverrides({
|
||||
double? lineHeightScaleFactor,
|
||||
double? letterSpacing,
|
||||
double? wordSpacing,
|
||||
required TextSpan textSpan,
|
||||
}) {
|
||||
if (lineHeightScaleFactor == null &&
|
||||
letterSpacing == null &&
|
||||
wordSpacing == null) {
|
||||
return textSpan;
|
||||
}
|
||||
return _applyTextStyleOverrides(
|
||||
TextStyle(
|
||||
height: lineHeightScaleFactor,
|
||||
letterSpacing: letterSpacing,
|
||||
wordSpacing: wordSpacing,
|
||||
),
|
||||
textSpan,
|
||||
);
|
||||
}
|
||||
|
||||
static TextSpan _applyTextStyleOverrides(
|
||||
TextStyle overrideTextStyle,
|
||||
TextSpan textSpan,
|
||||
) {
|
||||
return TextSpan(
|
||||
text: textSpan.text,
|
||||
children: textSpan.children?.map((InlineSpan child) {
|
||||
if (child is TextSpan && child.runtimeType == TextSpan) {
|
||||
return _applyTextStyleOverrides(overrideTextStyle, child);
|
||||
}
|
||||
return child;
|
||||
}).toList(),
|
||||
style: textSpan.style?.merge(overrideTextStyle) ?? overrideTextStyle,
|
||||
recognizer: textSpan.recognizer,
|
||||
mouseCursor: textSpan.mouseCursor,
|
||||
onEnter: textSpan.onEnter,
|
||||
onExit: textSpan.onExit,
|
||||
semanticsLabel: textSpan.semanticsLabel,
|
||||
semanticsIdentifier: textSpan.semanticsIdentifier,
|
||||
locale: textSpan.locale,
|
||||
spellOut: textSpan.spellOut,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
149
lib/common/widgets/more_text/paragraph_more.dart
Normal file
149
lib/common/widgets/more_text/paragraph_more.dart
Normal file
@@ -0,0 +1,149 @@
|
||||
import 'package:flutter/gestures.dart' show TapGestureRecognizer;
|
||||
import 'package:flutter/rendering.dart'
|
||||
show
|
||||
Size,
|
||||
Color,
|
||||
Offset,
|
||||
TextSpan,
|
||||
TextStyle,
|
||||
InlineSpan,
|
||||
TextScaler,
|
||||
TextPainter,
|
||||
VoidCallback,
|
||||
HitTestEntry,
|
||||
RenderParagraph,
|
||||
PaintingContext,
|
||||
BoxHitTestResult,
|
||||
RenderComparison;
|
||||
|
||||
class RenderParagraphMore extends RenderParagraph {
|
||||
RenderParagraphMore(
|
||||
super.text, {
|
||||
super.textAlign,
|
||||
required super.textDirection,
|
||||
super.softWrap,
|
||||
super.overflow,
|
||||
// ignore: deprecated_member_use
|
||||
super.textScaleFactor,
|
||||
super.textScaler,
|
||||
super.maxLines,
|
||||
super.locale,
|
||||
super.strutStyle,
|
||||
super.textWidthBasis,
|
||||
super.textHeightBehavior,
|
||||
super.children,
|
||||
super.selectionColor,
|
||||
super.registrar,
|
||||
required this._primary,
|
||||
this._onShowMore,
|
||||
});
|
||||
|
||||
Color _primary;
|
||||
|
||||
VoidCallback? _onShowMore;
|
||||
set onShowMore(VoidCallback? onShowMore) {
|
||||
if (_onShowMore != onShowMore) {
|
||||
_onShowMore = onShowMore;
|
||||
_tapGestureRecognizer?.onTap = onShowMore;
|
||||
}
|
||||
}
|
||||
|
||||
TapGestureRecognizer? _tapGestureRecognizer;
|
||||
|
||||
TextSpan _moreTextSpan([TextStyle? style]) => TextSpan(
|
||||
style: (style ?? text.style!).copyWith(color: _primary),
|
||||
text: '查看更多',
|
||||
recognizer: _tapGestureRecognizer,
|
||||
);
|
||||
TextPainter? _morePainter;
|
||||
|
||||
bool didOverflowHeight = false;
|
||||
|
||||
@override
|
||||
set textScaler(TextScaler value) {
|
||||
if (textPainter.textScaler == value) {
|
||||
return;
|
||||
}
|
||||
_morePainter
|
||||
?..textScaler = value
|
||||
..layout();
|
||||
super.textScaler = value;
|
||||
}
|
||||
|
||||
void setText(({InlineSpan text, Color primary}) params) {
|
||||
final newText = params.text;
|
||||
_primary = params.primary;
|
||||
if (_morePainter != null) {
|
||||
final textSpan = _moreTextSpan(newText.style);
|
||||
switch (_morePainter!.text!.compareTo(textSpan)) {
|
||||
case RenderComparison.paint:
|
||||
_morePainter!.text = textSpan;
|
||||
case RenderComparison.layout:
|
||||
_morePainter!
|
||||
..text = textSpan
|
||||
..layout();
|
||||
default:
|
||||
}
|
||||
}
|
||||
super.text = newText;
|
||||
}
|
||||
|
||||
@override
|
||||
void performLayout() {
|
||||
super.performLayout();
|
||||
|
||||
if (didOverflowHeight = textPainter.didExceedMaxLines) {
|
||||
if (_onShowMore != null) {
|
||||
_tapGestureRecognizer ??= TapGestureRecognizer()..onTap = _onShowMore;
|
||||
}
|
||||
_morePainter ??= TextPainter(
|
||||
text: _moreTextSpan(),
|
||||
textDirection: textDirection,
|
||||
textScaler: textScaler,
|
||||
locale: locale,
|
||||
)..layout(maxWidth: constraints.maxWidth);
|
||||
size = Size(
|
||||
size.width,
|
||||
constraints.constrainHeight(size.height + _morePainter!.height),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
super.paint(context, offset);
|
||||
|
||||
if (didOverflowHeight) {
|
||||
_morePainter?.paint(
|
||||
context.canvas,
|
||||
offset + Offset(0, textPainter.height),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
|
||||
if (_tapGestureRecognizer != null) {
|
||||
if (_morePainter != null) {
|
||||
if (position.dx < _morePainter!.width) {
|
||||
final height = textPainter.height;
|
||||
if (position.dy > height &&
|
||||
position.dy < height + _morePainter!.height) {
|
||||
result.add(HitTestEntry(_morePainter!.text as TextSpan));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.hitTestChildren(result, position: position);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tapGestureRecognizer?.dispose();
|
||||
_tapGestureRecognizer = null;
|
||||
_morePainter?.dispose();
|
||||
_morePainter = null;
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
72
lib/common/widgets/more_text/rich_text_more.dart
Normal file
72
lib/common/widgets/more_text/rich_text_more.dart
Normal file
@@ -0,0 +1,72 @@
|
||||
import 'package:PiliPlus/common/widgets/more_text/paragraph_more.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class RichTextMore extends RichText {
|
||||
RichTextMore({
|
||||
super.key,
|
||||
required super.text,
|
||||
super.textAlign,
|
||||
super.textDirection,
|
||||
super.softWrap,
|
||||
super.overflow,
|
||||
// ignore: deprecated_member_use
|
||||
super.textScaleFactor,
|
||||
super.textScaler,
|
||||
super.maxLines,
|
||||
super.locale,
|
||||
super.strutStyle,
|
||||
super.textWidthBasis,
|
||||
super.textHeightBehavior,
|
||||
super.selectionRegistrar,
|
||||
super.selectionColor,
|
||||
required this.primary,
|
||||
this.onShowMore,
|
||||
});
|
||||
|
||||
final Color primary;
|
||||
|
||||
final VoidCallback? onShowMore;
|
||||
|
||||
@override
|
||||
RenderParagraphMore createRenderObject(BuildContext context) {
|
||||
return RenderParagraphMore(
|
||||
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,
|
||||
RenderParagraphMore renderObject,
|
||||
) {
|
||||
renderObject
|
||||
..setText((text: text, primary: primary))
|
||||
..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
|
||||
..onShowMore = onShowMore;
|
||||
}
|
||||
}
|
||||
@@ -73,6 +73,8 @@ $ScrollableGesturePatch = "lib/scripts/scrollable_gesture.patch"
|
||||
|
||||
$DraggableScrollableSheetPatch = "lib/scripts/draggable_scrollable_sheet.patch"
|
||||
|
||||
$TextPatch = "lib/scripts/text.patch"
|
||||
|
||||
# TODO: remove
|
||||
# https://github.com/flutter/flutter/issues/124078
|
||||
# https://github.com/flutter/flutter/pull/183261
|
||||
@@ -108,7 +110,7 @@ $patches = @($ModalBarrierPatch, $TextSelectionPatch, $MouseCursorPatch,
|
||||
$PopupMenuPatch, $FABPatch, $NullSafetySelectableRegionPatch,
|
||||
$SelectableRegionPatch, $EditableTextPatch, $TextFieldPatch,
|
||||
$ScrollPositionPatch, $ScrollablePatch, $ScrollableGesturePatch,
|
||||
$DraggableScrollableSheetPatch, $ScaffoldPatch)
|
||||
$DraggableScrollableSheetPatch, $ScaffoldPatch, $TextPatch)
|
||||
|
||||
switch ($platform.ToLower()) {
|
||||
"android" {
|
||||
|
||||
26
lib/scripts/text.patch
Normal file
26
lib/scripts/text.patch
Normal file
@@ -0,0 +1,26 @@
|
||||
diff --git a/packages/flutter/lib/src/rendering/paragraph.dart b/packages/flutter/lib/src/rendering/paragraph.dart
|
||||
index 97a41dafb8d..7d807e1ad2c 100644
|
||||
--- a/packages/flutter/lib/src/rendering/paragraph.dart
|
||||
+++ b/packages/flutter/lib/src/rendering/paragraph.dart
|
||||
@@ -385,6 +385,7 @@ class RenderParagraph extends RenderBox
|
||||
);
|
||||
|
||||
final TextPainter _textPainter;
|
||||
+ TextPainter get textPainter => _textPainter;
|
||||
|
||||
// Currently, computing min/max intrinsic width/height will destroy state
|
||||
// inside the painter. Instead of calling _layout again to get back the correct
|
||||
diff --git a/packages/flutter/lib/src/widgets/text.dart b/packages/flutter/lib/src/widgets/text.dart
|
||||
index a3aa5fde862..51601f730af 100644
|
||||
--- a/packages/flutter/lib/src/widgets/text.dart
|
||||
+++ b/packages/flutter/lib/src/widgets/text.dart
|
||||
@@ -1503,6 +1503,9 @@ typedef _SelectionInfo = ({int contentLength, SelectedContentRange? range});
|
||||
// must also be updated.
|
||||
// TODO(Renzo-Olivares): Remove after investigating a solution for overriding all
|
||||
// styles for children in an [InlineSpan] tree, see: https://github.com/flutter/flutter/issues/177952.
|
||||
+
|
||||
+typedef OverridingTextStyleTextSpanUtils = _OverridingTextStyleTextSpanUtils;
|
||||
+
|
||||
class _OverridingTextStyleTextSpanUtils {
|
||||
static TextSpan applyTextSpacingOverrides({
|
||||
double? lineHeightScaleFactor,
|
||||
Reference in New Issue
Block a user