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';
|
/// @docImport 'widget_span.dart';
|
||||||
library;
|
library;
|
||||||
|
|
||||||
import 'dart:math';
|
|
||||||
import 'dart:ui' as ui show TextHeightBehavior;
|
import 'dart:ui' as ui show TextHeightBehavior;
|
||||||
|
|
||||||
import 'package:PiliPlus/common/widgets/flutter/text/paragraph.dart';
|
import 'package:PiliPlus/common/widgets/more_text/rich_text_more.dart';
|
||||||
import 'package:PiliPlus/common/widgets/flutter/text/rich_text.dart';
|
import 'package:flutter/material.dart' hide Text;
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart' hide Text, RichText;
|
|
||||||
import 'package:flutter/rendering.dart' hide RenderParagraph;
|
|
||||||
|
|
||||||
/// A run of text with a single style.
|
/// A run of text with a single style.
|
||||||
///
|
///
|
||||||
@@ -395,7 +391,7 @@ class Text extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
final double? wordSpacing = MediaQuery.maybeWordSpacingOverrideOf(context);
|
final double? wordSpacing = MediaQuery.maybeWordSpacingOverrideOf(context);
|
||||||
final TextSpan effectiveTextSpan =
|
final TextSpan effectiveTextSpan =
|
||||||
_OverridingTextStyleTextSpanUtils.applyTextSpacingOverrides(
|
OverridingTextStyleTextSpanUtils.applyTextSpacingOverrides(
|
||||||
lineHeightScaleFactor: lineHeightScaleFactor,
|
lineHeightScaleFactor: lineHeightScaleFactor,
|
||||||
letterSpacing: letterSpacing,
|
letterSpacing: letterSpacing,
|
||||||
wordSpacing: wordSpacing,
|
wordSpacing: wordSpacing,
|
||||||
@@ -409,7 +405,6 @@ class Text extends StatelessWidget {
|
|||||||
final StrutStyle? effectiveStrutStyle = strutStyle?.merge(
|
final StrutStyle? effectiveStrutStyle = strutStyle?.merge(
|
||||||
StrutStyle(height: lineHeightScaleFactor),
|
StrutStyle(height: lineHeightScaleFactor),
|
||||||
);
|
);
|
||||||
final SelectionRegistrar? registrar = SelectionContainer.maybeOf(context);
|
|
||||||
final TextScaler textScaler = switch ((this.textScaler, textScaleFactor)) {
|
final TextScaler textScaler = switch ((this.textScaler, textScaleFactor)) {
|
||||||
(final TextScaler textScaler, _) => textScaler,
|
(final TextScaler textScaler, _) => textScaler,
|
||||||
// For unmigrated apps, fall back to textScaleFactor.
|
// For unmigrated apps, fall back to textScaleFactor.
|
||||||
@@ -418,68 +413,31 @@ class Text extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
(null, null) => MediaQuery.textScalerOf(context),
|
(null, null) => MediaQuery.textScalerOf(context),
|
||||||
};
|
};
|
||||||
late Widget result;
|
Widget result = RichTextMore(
|
||||||
if (registrar != null) {
|
textAlign: textAlign ?? defaultTextStyle.textAlign ?? TextAlign.start,
|
||||||
result = MouseRegion(
|
textDirection:
|
||||||
cursor:
|
textDirection, // RichText uses Directionality.of to obtain a default if this is null.
|
||||||
DefaultSelectionStyle.of(context).mouseCursor ??
|
locale:
|
||||||
SystemMouseCursors.text,
|
locale, // RichText uses Localizations.localeOf to obtain a default if this is null
|
||||||
child: _SelectableTextContainer(
|
softWrap: softWrap ?? defaultTextStyle.softWrap,
|
||||||
textAlign: textAlign ?? defaultTextStyle.textAlign ?? TextAlign.start,
|
overflow:
|
||||||
textDirection:
|
overflow ?? effectiveTextStyle?.overflow ?? defaultTextStyle.overflow,
|
||||||
textDirection, // RichText uses Directionality.of to obtain a default if this is null.
|
textScaler: textScaler,
|
||||||
locale:
|
maxLines: maxLines ?? defaultTextStyle.maxLines,
|
||||||
locale, // RichText uses Localizations.localeOf to obtain a default if this is null
|
strutStyle: effectiveStrutStyle,
|
||||||
softWrap: softWrap ?? defaultTextStyle.softWrap,
|
textWidthBasis: textWidthBasis ?? defaultTextStyle.textWidthBasis,
|
||||||
overflow:
|
textHeightBehavior:
|
||||||
overflow ??
|
textHeightBehavior ??
|
||||||
effectiveTextStyle?.overflow ??
|
defaultTextStyle.textHeightBehavior ??
|
||||||
defaultTextStyle.overflow,
|
DefaultTextHeightBehavior.maybeOf(context),
|
||||||
textScaler: textScaler,
|
selectionColor:
|
||||||
maxLines: maxLines ?? defaultTextStyle.maxLines,
|
selectionColor ??
|
||||||
strutStyle: effectiveStrutStyle,
|
DefaultSelectionStyle.of(context).selectionColor ??
|
||||||
textWidthBasis: textWidthBasis ?? defaultTextStyle.textWidthBasis,
|
DefaultSelectionStyle.defaultColor,
|
||||||
textHeightBehavior:
|
text: effectiveTextSpan,
|
||||||
textHeightBehavior ??
|
primary: primary,
|
||||||
defaultTextStyle.textHeightBehavior ??
|
onShowMore: onShowMore,
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (semanticsLabel != null || semanticsIdentifier != null) {
|
if (semanticsLabel != null || semanticsIdentifier != null) {
|
||||||
result = Semantics(
|
result = Semantics(
|
||||||
textDirection: textDirection,
|
textDirection: textDirection,
|
||||||
@@ -493,853 +451,4 @@ class Text extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
return result;
|
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"
|
$DraggableScrollableSheetPatch = "lib/scripts/draggable_scrollable_sheet.patch"
|
||||||
|
|
||||||
|
$TextPatch = "lib/scripts/text.patch"
|
||||||
|
|
||||||
# TODO: remove
|
# TODO: remove
|
||||||
# https://github.com/flutter/flutter/issues/124078
|
# https://github.com/flutter/flutter/issues/124078
|
||||||
# https://github.com/flutter/flutter/pull/183261
|
# https://github.com/flutter/flutter/pull/183261
|
||||||
@@ -108,7 +110,7 @@ $patches = @($ModalBarrierPatch, $TextSelectionPatch, $MouseCursorPatch,
|
|||||||
$PopupMenuPatch, $FABPatch, $NullSafetySelectableRegionPatch,
|
$PopupMenuPatch, $FABPatch, $NullSafetySelectableRegionPatch,
|
||||||
$SelectableRegionPatch, $EditableTextPatch, $TextFieldPatch,
|
$SelectableRegionPatch, $EditableTextPatch, $TextFieldPatch,
|
||||||
$ScrollPositionPatch, $ScrollablePatch, $ScrollableGesturePatch,
|
$ScrollPositionPatch, $ScrollablePatch, $ScrollableGesturePatch,
|
||||||
$DraggableScrollableSheetPatch, $ScaffoldPatch)
|
$DraggableScrollableSheetPatch, $ScaffoldPatch, $TextPatch)
|
||||||
|
|
||||||
switch ($platform.ToLower()) {
|
switch ($platform.ToLower()) {
|
||||||
"android" {
|
"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