opt dyn text

Signed-off-by: dom <githubaccount56556@proton.me>
This commit is contained in:
dom
2026-08-01 09:49:25 +08:00
parent 946c7a82f4
commit 5c0c6b3cfd
7 changed files with 278 additions and 5133 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -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.
///
/// ![](https://flutter.github.io/assets-for-api-docs/assets/widgets/rich_text.png)
///
/// ```dart
/// RichText(
/// text: TextSpan(
/// text: 'Hello ',
/// style: DefaultTextStyle.of(context).style,
/// children: const <TextSpan>[
/// TextSpan(text: 'bold', style: TextStyle(fontWeight: FontWeight.bold)),
/// TextSpan(text: ' world!'),
/// ],
/// ),
/// )
/// ```
/// {@end-tool}
///
/// ## Selections
///
/// To make this [RichText] Selectable, the [RichText] needs to be in the
/// subtree of a [SelectionArea] or [SelectableRegion] and a
/// [SelectionRegistrar] needs to be assigned to the
/// [RichText.selectionRegistrar]. One can use
/// [SelectionContainer.maybeOf] to get the [SelectionRegistrar] from a
/// context. This enables users to select the text in [RichText]s with mice or
/// touch events.
///
/// The [selectionColor] also needs to be set if the selection is enabled to
/// draw the selection highlights.
///
/// {@tool snippet}
///
/// This sample demonstrates how to assign a [SelectionRegistrar] for RichTexts
/// in the SelectionArea subtree.
///
/// ![](https://flutter.github.io/assets-for-api-docs/assets/widgets/rich_text.png)
///
/// ```dart
/// RichText(
/// text: const TextSpan(text: 'Hello'),
/// selectionRegistrar: SelectionContainer.maybeOf(context),
/// selectionColor: const Color(0xAF6694e8),
/// )
/// ```
/// {@end-tool}
///
/// See also:
///
/// * [TextStyle], which discusses how to style text.
/// * [TextSpan], which is used to describe the text in a paragraph.
/// * [Text], which automatically applies the ambient styles described by a
/// [DefaultTextStyle] to a single string.
/// * [Text.rich], a const text widget that provides similar functionality
/// as [RichText]. [Text.rich] will inherit [TextStyle] from [DefaultTextStyle].
/// * [SelectableRegion], which provides an overview of the selection system.
class RichText extends MultiChildRenderObjectWidget {
/// Creates a paragraph of rich text.
///
/// The [maxLines] property may be null (and indeed defaults to null), but if
/// it is not null, it must be greater than zero.
///
/// The [textDirection], if null, defaults to the ambient [Directionality],
/// which in that case must not be null.
RichText({
super.key,
required this.text,
this.textAlign = TextAlign.start,
this.textDirection,
this.softWrap = true,
this.overflow = TextOverflow.clip,
@Deprecated(
'Use textScaler instead. '
'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. '
'This feature was deprecated after v3.12.0-2.0.pre.',
)
double textScaleFactor = 1.0,
TextScaler textScaler = TextScaler.noScaling,
this.maxLines,
this.locale,
this.strutStyle,
this.textWidthBasis = TextWidthBasis.parent,
this.textHeightBehavior,
this.selectionRegistrar,
this.selectionColor,
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,
),
);
}
}

View File

@@ -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,13 +413,7 @@ class Text extends StatelessWidget {
), ),
(null, null) => MediaQuery.textScalerOf(context), (null, null) => MediaQuery.textScalerOf(context),
}; };
late Widget result; Widget result = RichTextMore(
if (registrar != null) {
result = MouseRegion(
cursor:
DefaultSelectionStyle.of(context).mouseCursor ??
SystemMouseCursors.text,
child: _SelectableTextContainer(
textAlign: textAlign ?? defaultTextStyle.textAlign ?? TextAlign.start, textAlign: textAlign ?? defaultTextStyle.textAlign ?? TextAlign.start,
textDirection: textDirection:
textDirection, // RichText uses Directionality.of to obtain a default if this is null. textDirection, // RichText uses Directionality.of to obtain a default if this is null.
@@ -432,37 +421,7 @@ class Text extends StatelessWidget {
locale, // RichText uses Localizations.localeOf to obtain a default if this is null locale, // RichText uses Localizations.localeOf to obtain a default if this is null
softWrap: softWrap ?? defaultTextStyle.softWrap, softWrap: softWrap ?? defaultTextStyle.softWrap,
overflow: overflow:
overflow ?? overflow ?? effectiveTextStyle?.overflow ?? defaultTextStyle.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, textScaler: textScaler,
maxLines: maxLines ?? defaultTextStyle.maxLines, maxLines: maxLines ?? defaultTextStyle.maxLines,
strutStyle: effectiveStrutStyle, strutStyle: effectiveStrutStyle,
@@ -479,7 +438,6 @@ class Text extends StatelessWidget {
primary: primary, primary: primary,
onShowMore: onShowMore, 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,
);
}
} }

View 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();
}
}

View 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;
}
}

View File

@@ -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
View 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,