mirror of
https://github.com/bggRGjQaUbCoE/PiliPlus.git
synced 2026-07-29 06:50:12 +08:00
@@ -54,33 +54,34 @@ void _showEmoteDialog(ModuleDynamicModel? moduleDynamic) {
|
||||
builder: (context) => Dialog(
|
||||
child: Padding(
|
||||
padding: const .symmetric(horizontal: 20, vertical: 16),
|
||||
child: SingleChildScrollView(
|
||||
child: SelectionText.rich(
|
||||
TextSpan(
|
||||
children: emotes!.entries.mapIndexed(
|
||||
(i, e) {
|
||||
final emoji = e.value;
|
||||
final size = emoji.size * 25.0;
|
||||
return TextSpan(
|
||||
children: [
|
||||
if (i != 0) const TextSpan(text: '\n\n'),
|
||||
EmoteSpan(
|
||||
rawText: Style.placeHolder,
|
||||
child: NetworkImgLayer(
|
||||
src: emoji.url,
|
||||
type: .emote,
|
||||
width: size,
|
||||
height: size,
|
||||
child: SelectionArea(
|
||||
contextMenuBuilder: openUrlMenuBuilder,
|
||||
child: SingleChildScrollView(
|
||||
child: Text.rich(
|
||||
TextSpan(
|
||||
children: emotes!.entries.mapIndexed(
|
||||
(i, e) {
|
||||
final emoji = e.value;
|
||||
final size = emoji.size * 25.0;
|
||||
return TextSpan(
|
||||
children: [
|
||||
if (i != 0) const TextSpan(text: '\n\n'),
|
||||
WidgetSpan(
|
||||
child: NetworkImgLayer(
|
||||
src: emoji.url,
|
||||
type: .emote,
|
||||
width: size,
|
||||
height: size,
|
||||
),
|
||||
),
|
||||
),
|
||||
TextSpan(text: '\n${e.key}\n${emoji.url}'),
|
||||
],
|
||||
);
|
||||
},
|
||||
).toList(),
|
||||
TextSpan(text: '\n${e.key}\n${emoji.url}'),
|
||||
],
|
||||
);
|
||||
},
|
||||
).toList(),
|
||||
),
|
||||
style: const TextStyle(fontSize: 15, height: 1.7),
|
||||
),
|
||||
contextMenuBuilder: openUrlMenuBuilder,
|
||||
style: const TextStyle(fontSize: 15, height: 1.7),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -94,11 +95,13 @@ void _showTextDialog(String text) {
|
||||
builder: (context) => Dialog(
|
||||
child: Padding(
|
||||
padding: const .symmetric(horizontal: 20, vertical: 16),
|
||||
child: SingleChildScrollView(
|
||||
child: SelectionText(
|
||||
text,
|
||||
contextMenuBuilder: openUrlMenuBuilder,
|
||||
style: const TextStyle(fontSize: 15, height: 1.7),
|
||||
child: SelectionArea(
|
||||
contextMenuBuilder: openUrlMenuBuilder,
|
||||
child: SingleChildScrollView(
|
||||
child: Text(
|
||||
text,
|
||||
style: const TextStyle(fontSize: 15, height: 1.7),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -12,98 +12,99 @@ void showReplyCopyDialog(
|
||||
constraints: const BoxConstraints.tightFor(width: 380),
|
||||
child: Padding(
|
||||
padding: const .symmetric(horizontal: 20, vertical: 16),
|
||||
child: SingleChildScrollView(
|
||||
child: SelectionText.rich(
|
||||
showEmote
|
||||
? TextSpan(
|
||||
children: emotes.entries.mapIndexed(
|
||||
(i, e) {
|
||||
final emote = e.value;
|
||||
final size = emote.size.toInt() * 25.0;
|
||||
return TextSpan(
|
||||
child: SelectionArea(
|
||||
contextMenuBuilder: (_, state) {
|
||||
final buttonItems = state.contextMenuButtonItems;
|
||||
if (emotes.isNotEmpty) {
|
||||
buttonItems.insertOrAdd(
|
||||
3,
|
||||
ContextMenuButtonItem(
|
||||
label: showEmote ? '文本' : '表情',
|
||||
onPressed: () {
|
||||
state.hideAndClear();
|
||||
showEmote = !showEmote;
|
||||
(context as Element).markNeedsBuild();
|
||||
},
|
||||
),
|
||||
);
|
||||
state.addLaunchMenuIfNeeded(buttonItems, index: 4);
|
||||
}
|
||||
if (state.isUncollapsed) {
|
||||
buttonItems.add(
|
||||
ContextMenuButtonItem(
|
||||
onPressed: () {
|
||||
String text = RegExp.escape(state.selectedText!);
|
||||
if (ReplyGrpc.enableFilter) text = '|$text';
|
||||
|
||||
showConfirmDialog(
|
||||
context: context,
|
||||
title: const Text('是否确认评论过滤的变更:'),
|
||||
content: Text.rich(
|
||||
TextSpan(
|
||||
text: ReplyGrpc.replyRegExp.pattern,
|
||||
children: [
|
||||
if (i != 0) const TextSpan(text: '\n\n'),
|
||||
EmoteSpan(
|
||||
rawText: Style.placeHolder,
|
||||
child: NetworkImgLayer(
|
||||
src: emote.url,
|
||||
type: .emote,
|
||||
width: size,
|
||||
height: size,
|
||||
TextSpan(
|
||||
text: text,
|
||||
style: const TextStyle(
|
||||
color: Colors.green,
|
||||
fontWeight: .bold,
|
||||
),
|
||||
),
|
||||
TextSpan(text: '\n${e.key}\n${emote.url}'),
|
||||
],
|
||||
),
|
||||
),
|
||||
onConfirm: () {
|
||||
final filter = ReplyGrpc.replyRegExp.pattern + text;
|
||||
ReplyGrpc.replyRegExp = RegExp(
|
||||
filter,
|
||||
caseSensitive: true,
|
||||
);
|
||||
ReplyGrpc.enableFilter = true;
|
||||
GStorage.setting.put(
|
||||
SettingBoxKey.banWordForReply,
|
||||
filter,
|
||||
);
|
||||
SmartDialog.showToast('已保存');
|
||||
},
|
||||
).toList(),
|
||||
)
|
||||
: TextSpan(text: message),
|
||||
contextMenuBuilder: (_, state) {
|
||||
final buttonItems = state.contextMenuButtonItems;
|
||||
if (emotes.isNotEmpty) {
|
||||
buttonItems.insertOrAdd(
|
||||
3,
|
||||
ContextMenuButtonItem(
|
||||
label: showEmote ? '文本' : '表情',
|
||||
onPressed: () {
|
||||
state.hideAndClear();
|
||||
showEmote = !showEmote;
|
||||
(context as Element).markNeedsBuild();
|
||||
},
|
||||
),
|
||||
);
|
||||
state.addLaunchMenuIfNeeded(buttonItems, index: 4);
|
||||
}
|
||||
if (state.isUncollapsed) {
|
||||
buttonItems.add(
|
||||
ContextMenuButtonItem(
|
||||
onPressed: () {
|
||||
String text = RegExp.escape(state.selectedText!);
|
||||
if (ReplyGrpc.enableFilter) text = '|$text';
|
||||
|
||||
showConfirmDialog(
|
||||
context: context,
|
||||
title: const Text('是否确认评论过滤的变更:'),
|
||||
content: Text.rich(
|
||||
TextSpan(
|
||||
text: ReplyGrpc.replyRegExp.pattern,
|
||||
);
|
||||
},
|
||||
label: '加入过滤',
|
||||
),
|
||||
);
|
||||
}
|
||||
return AdaptiveTextSelectionToolbar.buttonItems(
|
||||
buttonItems: buttonItems,
|
||||
anchors: state.contextMenuAnchors,
|
||||
);
|
||||
},
|
||||
child: SingleChildScrollView(
|
||||
child: Text.rich(
|
||||
showEmote
|
||||
? TextSpan(
|
||||
children: emotes.entries.mapIndexed(
|
||||
(i, e) {
|
||||
final emote = e.value;
|
||||
final size = emote.size.toInt() * 25.0;
|
||||
return TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: text,
|
||||
style: const TextStyle(
|
||||
color: Colors.green,
|
||||
fontWeight: .bold,
|
||||
if (i != 0) const TextSpan(text: '\n\n'),
|
||||
WidgetSpan(
|
||||
child: NetworkImgLayer(
|
||||
src: emote.url,
|
||||
type: .emote,
|
||||
width: size,
|
||||
height: size,
|
||||
),
|
||||
),
|
||||
TextSpan(text: '\n${e.key}\n${emote.url}'),
|
||||
],
|
||||
),
|
||||
),
|
||||
onConfirm: () {
|
||||
final filter = ReplyGrpc.replyRegExp.pattern + text;
|
||||
ReplyGrpc.replyRegExp = RegExp(
|
||||
filter,
|
||||
caseSensitive: true,
|
||||
);
|
||||
ReplyGrpc.enableFilter = true;
|
||||
GStorage.setting.put(
|
||||
SettingBoxKey.banWordForReply,
|
||||
filter,
|
||||
);
|
||||
SmartDialog.showToast('已保存');
|
||||
},
|
||||
);
|
||||
},
|
||||
label: '加入过滤',
|
||||
),
|
||||
);
|
||||
}
|
||||
return AdaptiveTextSelectionToolbar.buttonItems(
|
||||
buttonItems: buttonItems,
|
||||
anchors: state.contextMenuAnchors,
|
||||
);
|
||||
},
|
||||
style: const TextStyle(fontSize: 15, height: 1.7),
|
||||
).toList(),
|
||||
)
|
||||
: TextSpan(text: message),
|
||||
style: const TextStyle(fontSize: 15, height: 1.7),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
46
lib/common/widgets/tap_region_surface.dart
Normal file
46
lib/common/widgets/tap_region_surface.dart
Normal file
@@ -0,0 +1,46 @@
|
||||
import 'package:flutter/rendering.dart' show BoxHitTestResult, HitTestEntry;
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class SelectionTapRegionSurface extends TapRegionSurface {
|
||||
const SelectionTapRegionSurface({
|
||||
super.key,
|
||||
required super.child,
|
||||
required this.isScrolling,
|
||||
});
|
||||
|
||||
final ValueGetter<bool> isScrolling;
|
||||
|
||||
@override
|
||||
RenderObject createRenderObject(BuildContext context) {
|
||||
return RenderSelectionTapRegionSurface(isScrolling);
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRenderObject(
|
||||
BuildContext context,
|
||||
RenderSelectionTapRegionSurface renderObject,
|
||||
) {
|
||||
renderObject.isScrolling = isScrolling;
|
||||
}
|
||||
}
|
||||
|
||||
class RenderSelectionTapRegionSurface extends RenderTapRegionSurface {
|
||||
RenderSelectionTapRegionSurface(this.isScrolling);
|
||||
|
||||
ValueGetter<bool> isScrolling;
|
||||
bool _isScrolling = false;
|
||||
|
||||
@override
|
||||
bool hitTest(BoxHitTestResult result, {required Offset position}) {
|
||||
_isScrolling = isScrolling();
|
||||
return super.hitTest(result, position: position);
|
||||
}
|
||||
|
||||
@override
|
||||
void handleEvent(PointerEvent event, HitTestEntry entry) {
|
||||
if (_isScrolling) {
|
||||
return;
|
||||
}
|
||||
super.handleEvent(event, entry);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
// 内容
|
||||
import 'package:PiliPlus/common/style.dart';
|
||||
import 'package:PiliPlus/common/widgets/custom_icon.dart';
|
||||
import 'package:PiliPlus/common/widgets/emote_span.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text/text.dart' as custom_text;
|
||||
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
|
||||
import 'package:PiliPlus/common/widgets/image_grid/image_grid_view.dart';
|
||||
|
||||
@@ -10,6 +10,7 @@ import 'package:PiliPlus/common/widgets/scroll_behavior.dart'
|
||||
import 'package:PiliPlus/common/widgets/scroll_physics.dart';
|
||||
import 'package:PiliPlus/common/widgets/sliver/sliver_floating_header.dart';
|
||||
import 'package:PiliPlus/common/widgets/sliver/sliver_to_box_adapter.dart';
|
||||
import 'package:PiliPlus/common/widgets/tap_region_surface.dart';
|
||||
import 'package:PiliPlus/http/constants.dart';
|
||||
import 'package:PiliPlus/http/dynamics.dart';
|
||||
import 'package:PiliPlus/http/loading_state.dart';
|
||||
@@ -105,15 +106,18 @@ class _DynamicDetailPageState
|
||||
);
|
||||
}
|
||||
|
||||
dynamic _scrollable;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scrollable = null;
|
||||
refreshController?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
final child = Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
appBar: _buildAppBar(),
|
||||
body: Padding(
|
||||
@@ -131,6 +135,10 @@ class _DynamicDetailPageState
|
||||
child: _buildBottom(),
|
||||
),
|
||||
);
|
||||
return SelectionTapRegionSurface(
|
||||
isScrolling: () => _scrollable?.shouldIgnorePointer ?? false,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
void _onEdit() {
|
||||
@@ -410,26 +418,33 @@ class _DynamicDetailPageState
|
||||
return child;
|
||||
}
|
||||
|
||||
Widget _buildDynPanel() {
|
||||
return SliverToBoxWithOffsetAdapter(
|
||||
offset: 55,
|
||||
onVisibilityChanged: controller.showTitle.call,
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
_scrollable = Scrollable.maybeOf(context);
|
||||
return DynamicPanel(
|
||||
item: controller.dynItem,
|
||||
isDetail: true,
|
||||
isDetailPortraitW: isPortrait,
|
||||
onSetPubSetting: controller.onSetPubSetting,
|
||||
onEdit: _onEdit,
|
||||
onSetReplySubject: controller.onSetReplySubject,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPortrait(double padding) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: padding),
|
||||
child: NestedScrollView(
|
||||
scrollBehavior: const NoOverscrollIndicator(),
|
||||
headerSliverBuilder: (context, innerBoxIsScrolled) {
|
||||
return [
|
||||
SliverToBoxWithOffsetAdapter(
|
||||
offset: 55,
|
||||
onVisibilityChanged: controller.showTitle.call,
|
||||
child: DynamicPanel(
|
||||
item: controller.dynItem,
|
||||
isDetail: true,
|
||||
isDetailPortraitW: isPortrait,
|
||||
onSetPubSetting: controller.onSetPubSetting,
|
||||
onEdit: _onEdit,
|
||||
onSetReplySubject: controller.onSetReplySubject,
|
||||
),
|
||||
),
|
||||
];
|
||||
return [_buildDynPanel()];
|
||||
},
|
||||
body: Column(
|
||||
children: [
|
||||
@@ -456,18 +471,7 @@ class _DynamicDetailPageState
|
||||
left: padding,
|
||||
bottom: this.padding.bottom + 100,
|
||||
),
|
||||
sliver: SliverToBoxWithOffsetAdapter(
|
||||
offset: 55,
|
||||
onVisibilityChanged: controller.showTitle.call,
|
||||
child: DynamicPanel(
|
||||
item: controller.dynItem,
|
||||
isDetail: true,
|
||||
isDetailPortraitW: isPortrait,
|
||||
onSetPubSetting: controller.onSetPubSetting,
|
||||
onEdit: _onEdit,
|
||||
onSetReplySubject: controller.onSetReplySubject,
|
||||
),
|
||||
),
|
||||
sliver: _buildDynPanel(),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -25,7 +25,11 @@ List<SettingsModel> get privacySettings => [
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('账号模式详情'),
|
||||
content: SingleChildScrollView(child: _getAccountDetail(context)),
|
||||
content: SelectionArea(
|
||||
child: SingleChildScrollView(
|
||||
child: _getAccountDetail(context),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: Get.back,
|
||||
@@ -52,12 +56,10 @@ Widget _getAccountDetail(BuildContext context) {
|
||||
..add(Center(child: Text(i.title, style: theme.titleMedium)))
|
||||
..add(Text(url.join('\n')));
|
||||
}
|
||||
return SelectionArea(
|
||||
child: Column(
|
||||
mainAxisSize: .min,
|
||||
crossAxisAlignment: .start,
|
||||
spacing: 8,
|
||||
children: children,
|
||||
),
|
||||
return Column(
|
||||
spacing: 8,
|
||||
mainAxisSize: .min,
|
||||
crossAxisAlignment: .start,
|
||||
children: children,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,13 +7,11 @@ import 'package:PiliPlus/common/widgets/badge.dart';
|
||||
import 'package:PiliPlus/common/widgets/custom_icon.dart';
|
||||
import 'package:PiliPlus/common/widgets/dialog/dialog.dart';
|
||||
import 'package:PiliPlus/common/widgets/dialog/report.dart';
|
||||
import 'package:PiliPlus/common/widgets/emote_span.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text/text.dart' as custom_text;
|
||||
import 'package:PiliPlus/common/widgets/gesture/tap_gesture_recognizer.dart';
|
||||
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
|
||||
import 'package:PiliPlus/common/widgets/image_grid/image_grid_view.dart';
|
||||
import 'package:PiliPlus/common/widgets/pendant_avatar.dart';
|
||||
import 'package:PiliPlus/common/widgets/selection_text.dart';
|
||||
import 'package:PiliPlus/common/widgets/translucent_row.dart';
|
||||
import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart'
|
||||
show ReplyInfo, ReplyControl, Content, Url, ReplyControl_VoteOption, Emote;
|
||||
|
||||
@@ -45,7 +45,7 @@ $TextFieldPatch = "lib/scripts/text_field.patch"
|
||||
|
||||
$ScrollPositionPatch = "lib/scripts/scroll_position.patch"
|
||||
|
||||
$SelectionPlaceholderPatch = "lib/scripts/selection_placeholder.patch"
|
||||
$ScrollablePatch = "lib/scripts/scrollable.patch"
|
||||
|
||||
# TODO: remove
|
||||
# https://github.com/flutter/flutter/pull/183261
|
||||
@@ -80,7 +80,7 @@ $patches = @($ModalBarrierPatch, $TextSelectionPatch, $MouseCursorPatch,
|
||||
$ImageAnimPatch, $LayoutBuilderPatch, $NavigationDrawerPatch,
|
||||
$PopupMenuPatch, $FABPatch, $SelectableRegionPatch, $SelectableRegionSelectionPatch,
|
||||
$EditableTextPatch, $TextFieldPatch, $ScrollPositionPatch,
|
||||
$SelectionPlaceholderPatch)
|
||||
$ScrollablePatch)
|
||||
|
||||
switch ($platform.ToLower()) {
|
||||
"android" {
|
||||
|
||||
12
lib/scripts/scrollable.patch
Normal file
12
lib/scripts/scrollable.patch
Normal file
@@ -0,0 +1,12 @@
|
||||
diff --git a/packages/flutter/lib/src/widgets/scrollable.dart b/packages/flutter/lib/src/widgets/scrollable.dart
|
||||
index 9df2d633f16..f39e6760df9 100644
|
||||
--- a/packages/flutter/lib/src/widgets/scrollable.dart
|
||||
+++ b/packages/flutter/lib/src/widgets/scrollable.dart
|
||||
@@ -766,6 +766,7 @@ class ScrollableState extends State<Scrollable>
|
||||
Map<Type, GestureRecognizerFactory> _gestureRecognizers =
|
||||
const <Type, GestureRecognizerFactory>{};
|
||||
bool _shouldIgnorePointer = false;
|
||||
+ bool get shouldIgnorePointer => _shouldIgnorePointer;
|
||||
|
||||
bool? _lastCanDrag;
|
||||
Axis? _lastAxisDirection;
|
||||
@@ -1,21 +1,326 @@
|
||||
diff --git a/packages/flutter/lib/src/painting/inline_span.dart b/packages/flutter/lib/src/painting/inline_span.dart
|
||||
index 0c4a0be6177..77baa589699 100644
|
||||
--- a/packages/flutter/lib/src/painting/inline_span.dart
|
||||
+++ b/packages/flutter/lib/src/painting/inline_span.dart
|
||||
@@ -315,6 +315,21 @@ abstract class InlineSpan extends DiagnosticableTree {
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
+ (String, Map<int, String>) toPlainTextV2({
|
||||
+ bool includeSemanticsLabels = true,
|
||||
+ bool includePlaceholders = true,
|
||||
+ }) {
|
||||
+ final buffer = StringBuffer();
|
||||
+ final map = <int, String>{};
|
||||
+ computeToPlainTextV2(
|
||||
+ buffer,
|
||||
+ map,
|
||||
+ includeSemanticsLabels: includeSemanticsLabels,
|
||||
+ includePlaceholders: includePlaceholders,
|
||||
+ );
|
||||
+ return (buffer.toString(), map);
|
||||
+ }
|
||||
+
|
||||
/// Flattens the [InlineSpan] tree to a list of
|
||||
/// [InlineSpanSemanticsInformation] objects.
|
||||
///
|
||||
@@ -358,6 +373,14 @@ abstract class InlineSpan extends DiagnosticableTree {
|
||||
bool includePlaceholders = true,
|
||||
});
|
||||
|
||||
+ @protected
|
||||
+ void computeToPlainTextV2(
|
||||
+ StringBuffer buffer,
|
||||
+ Map<int, String> map, {
|
||||
+ bool includeSemanticsLabels = true,
|
||||
+ bool includePlaceholders = true,
|
||||
+ });
|
||||
+
|
||||
/// Returns the UTF-16 code unit at the given `index` in the flattened string.
|
||||
///
|
||||
/// This only accounts for the [TextSpan.text] values and ignores [PlaceholderSpan]s.
|
||||
diff --git a/packages/flutter/lib/src/painting/placeholder_span.dart b/packages/flutter/lib/src/painting/placeholder_span.dart
|
||||
index 110ff860310..7ad808d05bc 100644
|
||||
--- a/packages/flutter/lib/src/painting/placeholder_span.dart
|
||||
+++ b/packages/flutter/lib/src/painting/placeholder_span.dart
|
||||
@@ -45,6 +45,7 @@ abstract class PlaceholderSpan extends InlineSpan {
|
||||
this.alignment = ui.PlaceholderAlignment.bottom,
|
||||
this.baseline,
|
||||
super.style,
|
||||
+ this.rawText,
|
||||
});
|
||||
|
||||
/// The unicode character to represent a placeholder.
|
||||
@@ -61,6 +62,8 @@ abstract class PlaceholderSpan extends InlineSpan {
|
||||
/// This is ignored when using other alignment modes.
|
||||
final TextBaseline? baseline;
|
||||
|
||||
+ final String? rawText;
|
||||
+
|
||||
/// [PlaceholderSpan]s are flattened to a `0xFFFC` object replacement character in the
|
||||
/// plain text representation when `includePlaceholders` is true.
|
||||
@override
|
||||
@@ -74,6 +77,21 @@ abstract class PlaceholderSpan extends InlineSpan {
|
||||
}
|
||||
}
|
||||
|
||||
+ @override
|
||||
+ void computeToPlainTextV2(
|
||||
+ StringBuffer buffer,
|
||||
+ Map<int, String> map, {
|
||||
+ bool includeSemanticsLabels = true,
|
||||
+ bool includePlaceholders = true,
|
||||
+ }) {
|
||||
+ if (includePlaceholders) {
|
||||
+ if (rawText != null) {
|
||||
+ map[buffer.length] = rawText!;
|
||||
+ }
|
||||
+ buffer.writeCharCode(placeholderCodeUnit);
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
@override
|
||||
void computeSemanticsInformation(List<InlineSpanSemanticsInformation> collector) {
|
||||
collector.add(InlineSpanSemanticsInformation.placeholder);
|
||||
diff --git a/packages/flutter/lib/src/painting/text_span.dart b/packages/flutter/lib/src/painting/text_span.dart
|
||||
index 09fba7fc4fb..57ee469358e 100644
|
||||
--- a/packages/flutter/lib/src/painting/text_span.dart
|
||||
+++ b/packages/flutter/lib/src/painting/text_span.dart
|
||||
@@ -398,6 +398,31 @@ class TextSpan extends InlineSpan implements HitTestTarget, MouseTrackerAnnotati
|
||||
}
|
||||
}
|
||||
|
||||
+ @override
|
||||
+ void computeToPlainTextV2(
|
||||
+ StringBuffer buffer,
|
||||
+ Map<int, String> map, {
|
||||
+ bool includeSemanticsLabels = true,
|
||||
+ bool includePlaceholders = true,
|
||||
+ }) {
|
||||
+ assert(debugAssertIsValid());
|
||||
+ if (semanticsLabel != null && includeSemanticsLabels) {
|
||||
+ buffer.write(semanticsLabel);
|
||||
+ } else if (text != null) {
|
||||
+ buffer.write(text);
|
||||
+ }
|
||||
+ if (children != null) {
|
||||
+ for (final InlineSpan child in children!) {
|
||||
+ child.computeToPlainTextV2(
|
||||
+ buffer,
|
||||
+ map,
|
||||
+ includeSemanticsLabels: includeSemanticsLabels,
|
||||
+ includePlaceholders: includePlaceholders,
|
||||
+ );
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
@override
|
||||
void computeSemanticsInformation(
|
||||
List<InlineSpanSemanticsInformation> collector, {
|
||||
diff --git a/packages/flutter/lib/src/rendering/paragraph.dart b/packages/flutter/lib/src/rendering/paragraph.dart
|
||||
index 97a41dafb8d..0be1766148e 100644
|
||||
index 97a41dafb8d..266dd6f0283 100644
|
||||
--- a/packages/flutter/lib/src/rendering/paragraph.dart
|
||||
+++ b/packages/flutter/lib/src/rendering/paragraph.dart
|
||||
@@ -1925,9 +1925,9 @@ class _SelectableFragment
|
||||
final Matrix4 transform = paragraph.getTransformTo(null);
|
||||
transform.invert();
|
||||
final Offset localPosition = MatrixUtils.transformPoint(transform, globalPosition);
|
||||
- if (_rect.isEmpty) {
|
||||
- return SelectionUtils.getResultBasedOnRect(_rect, localPosition);
|
||||
@@ -502,27 +502,17 @@ class RenderParagraph extends RenderBox
|
||||
}
|
||||
|
||||
List<_SelectableFragment> _getSelectableFragments() {
|
||||
- final String plainText = text.toPlainText(includeSemanticsLabels: false);
|
||||
- final result = <_SelectableFragment>[];
|
||||
- var start = 0;
|
||||
- while (start < plainText.length) {
|
||||
- int end = plainText.indexOf(_placeholderCharacter, start);
|
||||
- if (start != end) {
|
||||
- if (end == -1) {
|
||||
- end = plainText.length;
|
||||
- }
|
||||
- result.add(
|
||||
- _SelectableFragment(
|
||||
- paragraph: this,
|
||||
- range: TextRange(start: start, end: end),
|
||||
- fullText: plainText,
|
||||
- ),
|
||||
- );
|
||||
- start = end;
|
||||
- }
|
||||
- start += 1;
|
||||
- }
|
||||
+ // if (_rect.isEmpty) {
|
||||
+ // return SelectionUtils.getResultBasedOnRect(_rect, localPosition);
|
||||
+ // }
|
||||
final Offset adjustedOffset = SelectionUtils.adjustDragOffset(
|
||||
- return result;
|
||||
+ final (String plainText, Map<int, String> placeHolder) = text.toPlainTextV2(
|
||||
+ includeSemanticsLabels: false,
|
||||
+ );
|
||||
+ return [
|
||||
+ _SelectableFragment(
|
||||
+ paragraph: this,
|
||||
+ range: TextRange(start: 0, end: plainText.length),
|
||||
+ fullText: plainText,
|
||||
+ placeholder: placeHolder,
|
||||
+ ),
|
||||
+ ];
|
||||
}
|
||||
|
||||
/// Determines whether the given [Selectable] was created by this
|
||||
@@ -1467,7 +1457,7 @@ class RenderParagraph extends RenderBox
|
||||
class _SelectableFragment
|
||||
with Selectable, Diagnosticable, ChangeNotifier
|
||||
implements TextLayoutMetrics {
|
||||
- _SelectableFragment({required this.paragraph, required this.fullText, required this.range})
|
||||
+ _SelectableFragment({required this.paragraph, required this.fullText, required this.range, required this.placeholder})
|
||||
: assert(range.isValid && !range.isCollapsed && range.isNormalized) {
|
||||
if (kFlutterMemoryAllocationsEnabled) {
|
||||
ChangeNotifier.maybeDispatchObjectCreation(this);
|
||||
@@ -1478,6 +1468,7 @@ class _SelectableFragment
|
||||
final TextRange range;
|
||||
final RenderParagraph paragraph;
|
||||
final String fullText;
|
||||
+ final Map<int,String> placeholder;
|
||||
|
||||
TextPosition? _textSelectionStart;
|
||||
TextPosition? _textSelectionEnd;
|
||||
@@ -1509,18 +1500,36 @@ class _SelectableFragment
|
||||
final int selectionStart = _textSelectionStart!.offset;
|
||||
final int selectionEnd = _textSelectionEnd!.offset;
|
||||
final bool isReversed = selectionStart > selectionEnd;
|
||||
- final Offset startOffsetInParagraphCoordinates = paragraph._getOffsetForPosition(
|
||||
- TextPosition(offset: selectionStart),
|
||||
- );
|
||||
- final Offset endOffsetInParagraphCoordinates = selectionStart == selectionEnd
|
||||
- ? startOffsetInParagraphCoordinates
|
||||
- : paragraph._getOffsetForPosition(TextPosition(offset: selectionEnd));
|
||||
+
|
||||
final flipHandles = isReversed != (TextDirection.rtl == paragraph.textDirection);
|
||||
final selection = TextSelection(baseOffset: selectionStart, extentOffset: selectionEnd);
|
||||
+ final List<ui.TextBox> boxes = paragraph.getBoxesForSelection(selection, boxHeightStyle: .max);
|
||||
final selectionRects = <Rect>[];
|
||||
- for (final TextBox textBox in paragraph.getBoxesForSelection(selection)) {
|
||||
+ for (final textBox in boxes) {
|
||||
selectionRects.add(textBox.toRect());
|
||||
}
|
||||
+
|
||||
+ final Offset startOffsetInParagraphCoordinates;
|
||||
+ final Offset endOffsetInParagraphCoordinates;
|
||||
+ if (boxes.isEmpty) {
|
||||
+ final Offset offset =
|
||||
+ paragraph._textPainter.getOffsetForCaret(selection.extent, Rect.zero) +
|
||||
+ Offset(0, paragraph._textPainter.getFullHeightForCaret(selection.extent, Rect.zero));
|
||||
+ startOffsetInParagraphCoordinates = endOffsetInParagraphCoordinates = Offset(
|
||||
+ clampDouble(offset.dx, 0, paragraph._textPainter.width),
|
||||
+ clampDouble(offset.dy, 0, paragraph._textPainter.height),
|
||||
+ );
|
||||
+ } else {
|
||||
+ startOffsetInParagraphCoordinates = Offset(
|
||||
+ clampDouble(boxes.first.start, 0, paragraph._textPainter.width),
|
||||
+ clampDouble(boxes.first.bottom, 0, paragraph._textPainter.height),
|
||||
+ );
|
||||
+ endOffsetInParagraphCoordinates = Offset(
|
||||
+ clampDouble(boxes.last.end, 0, paragraph._textPainter.width),
|
||||
+ clampDouble(boxes.last.bottom, 0, paragraph._textPainter.height),
|
||||
+ );
|
||||
+ }
|
||||
+
|
||||
final selectionCollapsed = selectionStart == selectionEnd;
|
||||
final (
|
||||
TextSelectionHandleType startSelectionHandleType,
|
||||
@@ -1564,12 +1573,14 @@ class _SelectableFragment
|
||||
result = _updateSelectionEdge(
|
||||
edgeUpdate.globalPosition,
|
||||
isEnd: edgeUpdate.type == SelectionEventType.endEdgeUpdate,
|
||||
+ isSingle: edgeUpdate.isSingle,
|
||||
);
|
||||
case TextGranularity.word:
|
||||
result = _updateSelectionEdgeByTextBoundary(
|
||||
edgeUpdate.globalPosition,
|
||||
isEnd: edgeUpdate.type == SelectionEventType.endEdgeUpdate,
|
||||
getTextBoundary: _getWordBoundaryAtPosition,
|
||||
+ isSingle: edgeUpdate.isSingle,
|
||||
);
|
||||
case TextGranularity.paragraph:
|
||||
result = _updateSelectionEdgeByMultiSelectableTextBoundary(
|
||||
@@ -1626,9 +1637,33 @@ class _SelectableFragment
|
||||
if (_textSelectionStart == null || _textSelectionEnd == null) {
|
||||
return null;
|
||||
}
|
||||
- final int start = math.min(_textSelectionStart!.offset, _textSelectionEnd!.offset);
|
||||
+ int start = math.min(_textSelectionStart!.offset, _textSelectionEnd!.offset);
|
||||
final int end = math.max(_textSelectionStart!.offset, _textSelectionEnd!.offset);
|
||||
- return SelectedContent(plainText: fullText.substring(start, end));
|
||||
+ final String selectedText;
|
||||
+ if (placeholder.isEmpty) {
|
||||
+ selectedText = fullText.substring(start, end);
|
||||
+ } else {
|
||||
+ final buffer = StringBuffer();
|
||||
+ for (final e in placeholder.entries) {
|
||||
+ final i = e.key;
|
||||
+ if (i < start) {
|
||||
+ continue;
|
||||
+ }
|
||||
+ if (i >= end) {
|
||||
+ break;
|
||||
+ }
|
||||
+ if (i != start) {
|
||||
+ buffer.write(fullText.substring(start, i));
|
||||
+ }
|
||||
+ buffer.write(e.value);
|
||||
+ start = i + 1;
|
||||
+ }
|
||||
+ if (start != end) {
|
||||
+ buffer.write(fullText.substring(start, end));
|
||||
+ }
|
||||
+ selectedText = buffer.toString();
|
||||
+ }
|
||||
+ return SelectedContent(plainText: selectedText);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -1848,6 +1883,7 @@ class _SelectableFragment
|
||||
Offset globalPosition, {
|
||||
required bool isEnd,
|
||||
required _TextBoundaryAtPosition getTextBoundary,
|
||||
+ bool isSingle = false,
|
||||
}) {
|
||||
// When the start/end edges are swapped, i.e. the start is after the end, and
|
||||
// the scrollable synthesizes an event for the opposite edge, this will potentially
|
||||
@@ -1866,6 +1902,7 @@ class _SelectableFragment
|
||||
_rect,
|
||||
localPosition,
|
||||
@@ -3568,7 +3568,10 @@ class _SelectableFragment
|
||||
direction: paragraph.textDirection,
|
||||
+ isSingle: isSingle,
|
||||
);
|
||||
|
||||
final TextPosition position = paragraph.getPositionForOffset(adjustedOffset);
|
||||
@@ -1920,7 +1957,7 @@ class _SelectableFragment
|
||||
return SelectionUtils.getResultBasedOnRect(_rect, localPosition);
|
||||
}
|
||||
|
||||
- SelectionResult _updateSelectionEdge(Offset globalPosition, {required bool isEnd}) {
|
||||
+ SelectionResult _updateSelectionEdge(Offset globalPosition, {required bool isEnd, bool isSingle = false}) {
|
||||
_setSelectionPosition(null, isEnd: isEnd);
|
||||
final Matrix4 transform = paragraph.getTransformTo(null);
|
||||
transform.invert();
|
||||
@@ -1932,11 +1969,21 @@ class _SelectableFragment
|
||||
_rect,
|
||||
localPosition,
|
||||
direction: paragraph.textDirection,
|
||||
+ isSingle: isSingle,
|
||||
);
|
||||
|
||||
final TextPosition position = _clampTextPosition(
|
||||
paragraph.getPositionForOffset(adjustedOffset),
|
||||
);
|
||||
+ // if (rawText != null) {
|
||||
+ // position = _clampTextPosition(
|
||||
+ // adjustedOffset.dx * 2 > _rect.left + _rect.right
|
||||
+ // ? TextPosition(offset: range.end)
|
||||
+ // : TextPosition(offset: range.start),
|
||||
+ // );
|
||||
+ // } else {
|
||||
+ // position = _clampTextPosition(paragraph.getPositionForOffset(adjustedOffset));
|
||||
+ // }
|
||||
_setSelectionPosition(position, isEnd: isEnd);
|
||||
if (position.offset == range.end) {
|
||||
return SelectionResult.next;
|
||||
@@ -3568,7 +3615,10 @@ class _SelectableFragment
|
||||
final selectionPaint = Paint()
|
||||
..style = PaintingStyle.fill
|
||||
..color = paragraph.selectionColor!;
|
||||
@@ -28,10 +333,19 @@ index 97a41dafb8d..0be1766148e 100644
|
||||
}
|
||||
}
|
||||
diff --git a/packages/flutter/lib/src/rendering/selection.dart b/packages/flutter/lib/src/rendering/selection.dart
|
||||
index a813e141dc4..78b9bb84c0e 100644
|
||||
index a813e141dc4..e5ef2103645 100644
|
||||
--- a/packages/flutter/lib/src/rendering/selection.dart
|
||||
+++ b/packages/flutter/lib/src/rendering/selection.dart
|
||||
@@ -114,6 +114,8 @@ abstract class SelectionHandler implements ValueListenable<SelectionGeometry> {
|
||||
@@ -6,6 +6,8 @@
|
||||
/// @docImport 'package:flutter/material.dart';
|
||||
library;
|
||||
|
||||
+import 'dart:math' as math;
|
||||
+
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:vector_math/vector_math_64.dart';
|
||||
|
||||
@@ -114,6 +116,8 @@ abstract class SelectionHandler implements ValueListenable<SelectionGeometry> {
|
||||
|
||||
/// The length of the content in this object.
|
||||
int get contentLength;
|
||||
@@ -40,7 +354,7 @@ index a813e141dc4..78b9bb84c0e 100644
|
||||
}
|
||||
|
||||
/// This class stores the range information of the selection under a [Selectable]
|
||||
@@ -236,6 +238,8 @@ class SelectedContent with Diagnosticable {
|
||||
@@ -236,6 +240,8 @@ class SelectedContent with Diagnosticable {
|
||||
/// See also:
|
||||
/// * [SelectableRegion], which provides an overview of selection system.
|
||||
mixin Selectable implements SelectionHandler {
|
||||
@@ -49,6 +363,53 @@ index a813e141dc4..78b9bb84c0e 100644
|
||||
/// {@macro flutter.rendering.RenderObject.getTransformTo}
|
||||
Matrix4 getTransformTo(RenderObject? ancestor);
|
||||
|
||||
@@ -356,10 +362,19 @@ abstract final class SelectionUtils {
|
||||
Rect targetRect,
|
||||
Offset point, {
|
||||
TextDirection direction = TextDirection.ltr,
|
||||
+ bool isSingle = false,
|
||||
}) {
|
||||
if (targetRect.contains(point)) {
|
||||
return point;
|
||||
}
|
||||
+
|
||||
+ if (isSingle || (point.dy >= targetRect.top && point.dy <= targetRect.bottom)) {
|
||||
+ return Offset(
|
||||
+ math.min(point.dx, targetRect.right),
|
||||
+ math.min(point.dy, targetRect.bottom - .1),
|
||||
+ );
|
||||
+ }
|
||||
+
|
||||
if (point.dy <= targetRect.top ||
|
||||
point.dy <= targetRect.bottom && point.dx <= targetRect.left) {
|
||||
// Area 1
|
||||
@@ -520,7 +535,7 @@ class SelectionEdgeUpdateEvent extends SelectionEvent {
|
||||
///
|
||||
/// The [granularity] contains the granularity which the selection edge should move by.
|
||||
/// This value defaults to [TextGranularity.character].
|
||||
- const SelectionEdgeUpdateEvent.forStart({
|
||||
+ SelectionEdgeUpdateEvent.forStart({
|
||||
required this.globalPosition,
|
||||
TextGranularity? granularity,
|
||||
}) : granularity = granularity ?? TextGranularity.character,
|
||||
@@ -532,7 +547,7 @@ class SelectionEdgeUpdateEvent extends SelectionEvent {
|
||||
///
|
||||
/// The [granularity] contains the granularity which the selection edge should move by.
|
||||
/// This value defaults to [TextGranularity.character].
|
||||
- const SelectionEdgeUpdateEvent.forEnd({
|
||||
+ SelectionEdgeUpdateEvent.forEnd({
|
||||
required this.globalPosition,
|
||||
TextGranularity? granularity,
|
||||
}) : granularity = granularity ?? TextGranularity.character,
|
||||
@@ -547,6 +562,8 @@ class SelectionEdgeUpdateEvent extends SelectionEvent {
|
||||
///
|
||||
/// Defaults to [TextGranularity.character].
|
||||
final TextGranularity granularity;
|
||||
+
|
||||
+ bool isSingle = false;
|
||||
}
|
||||
|
||||
/// Extends the start or end of the selection by a given [TextGranularity].
|
||||
diff --git a/packages/flutter/lib/src/widgets/scrollable.dart b/packages/flutter/lib/src/widgets/scrollable.dart
|
||||
index 9df2d633f16..54d30883245 100644
|
||||
--- a/packages/flutter/lib/src/widgets/scrollable.dart
|
||||
@@ -64,7 +425,7 @@ index 9df2d633f16..54d30883245 100644
|
||||
static const double _kDefaultDragTargetSize = 0;
|
||||
|
||||
diff --git a/packages/flutter/lib/src/widgets/selectable_region.dart b/packages/flutter/lib/src/widgets/selectable_region.dart
|
||||
index 59de8bae20b..39bfeb2b4f0 100644
|
||||
index 59de8bae20b..9ce4810aa68 100644
|
||||
--- a/packages/flutter/lib/src/widgets/selectable_region.dart
|
||||
+++ b/packages/flutter/lib/src/widgets/selectable_region.dart
|
||||
@@ -401,9 +401,11 @@ class SelectableRegionState extends State<SelectableRegion>
|
||||
@@ -209,7 +570,35 @@ index 59de8bae20b..39bfeb2b4f0 100644
|
||||
/// The set of [Selectable]s that have received start events.
|
||||
final Set<Selectable> _hasReceivedStartEvent = <Selectable>{};
|
||||
|
||||
@@ -2822,8 +2801,17 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai
|
||||
@@ -2271,6 +2250,18 @@ class StaticSelectionContainerDelegate extends MultiSelectableSelectionContainer
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
+ void handleSelectionEdgeUpdateEvent(SelectionEdgeUpdateEvent event) {
|
||||
+ event.isSingle = isSingle;
|
||||
+ }
|
||||
+
|
||||
+ @override
|
||||
+ SelectionResult dispatchSelectionEvent(SelectionEvent event) {
|
||||
+ if (event is SelectionEdgeUpdateEvent) {
|
||||
+ handleSelectionEdgeUpdateEvent(event);
|
||||
+ }
|
||||
+ return super.dispatchSelectionEvent(event);
|
||||
+ }
|
||||
+
|
||||
@override
|
||||
SelectionResult dispatchSelectionEventToChild(Selectable selectable, SelectionEvent event) {
|
||||
switch (event.type) {
|
||||
@@ -2367,6 +2358,8 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai
|
||||
/// Gets the list of [Selectable]s this delegate is managing.
|
||||
List<Selectable> selectables = <Selectable>[];
|
||||
|
||||
+ bool get isSingle => selectables.length == 1;
|
||||
+
|
||||
/// The number of additional pixels added to the selection handle drawable
|
||||
/// area.
|
||||
///
|
||||
@@ -2822,8 +2815,17 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai
|
||||
return null;
|
||||
}
|
||||
final buffer = StringBuffer();
|
||||
@@ -242,3 +631,30 @@ index 5d5a2386a7a..92e7d1eb70a 100644
|
||||
BuildContext? _selectionContainerContext;
|
||||
|
||||
/// Gets the paint transform from the [Selectable] child to
|
||||
diff --git a/packages/flutter/lib/src/widgets/text.dart b/packages/flutter/lib/src/widgets/text.dart
|
||||
index a3aa5fde862..8fbf6a1b16d 100644
|
||||
--- a/packages/flutter/lib/src/widgets/text.dart
|
||||
+++ b/packages/flutter/lib/src/widgets/text.dart
|
||||
@@ -992,6 +992,9 @@ class _SelectableTextContainerDelegate extends StaticSelectionContainerDelegate
|
||||
final GlobalKey _textKey;
|
||||
RenderParagraph get paragraph => _textKey.currentContext!.findRenderObject()! as RenderParagraph;
|
||||
|
||||
+ @override
|
||||
+ void handleSelectionEdgeUpdateEvent(SelectionEdgeUpdateEvent event) {}
|
||||
+
|
||||
@override
|
||||
SelectionResult handleSelectParagraph(SelectParagraphSelectionEvent event) {
|
||||
final SelectionResult result = _handleSelectParagraph(event);
|
||||
diff --git a/packages/flutter/lib/src/widgets/widget_span.dart b/packages/flutter/lib/src/widgets/widget_span.dart
|
||||
index 09da3b76525..4a83efb0392 100644
|
||||
--- a/packages/flutter/lib/src/widgets/widget_span.dart
|
||||
+++ b/packages/flutter/lib/src/widgets/widget_span.dart
|
||||
@@ -78,7 +78,7 @@ class WidgetSpan extends PlaceholderSpan {
|
||||
///
|
||||
/// A [TextStyle] may be provided with the [style] property, but only the
|
||||
/// decoration, foreground, background, and spacing options will be used.
|
||||
- const WidgetSpan({required this.child, super.alignment, super.baseline, super.style})
|
||||
+ const WidgetSpan({required this.child, super.alignment, super.baseline, super.style, super.rawText})
|
||||
: assert(
|
||||
baseline != null ||
|
||||
!(identical(alignment, ui.PlaceholderAlignment.aboveBaseline) ||
|
||||
|
||||
@@ -1,222 +0,0 @@
|
||||
diff --git a/packages/flutter/lib/src/painting/inline_span.dart b/packages/flutter/lib/src/painting/inline_span.dart
|
||||
index 0c4a0be6177..77baa589699 100644
|
||||
--- a/packages/flutter/lib/src/painting/inline_span.dart
|
||||
+++ b/packages/flutter/lib/src/painting/inline_span.dart
|
||||
@@ -315,6 +315,21 @@ abstract class InlineSpan extends DiagnosticableTree {
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
+ (String, Map<int, String>) toPlainTextV2({
|
||||
+ bool includeSemanticsLabels = true,
|
||||
+ bool includePlaceholders = true,
|
||||
+ }) {
|
||||
+ final buffer = StringBuffer();
|
||||
+ final map = <int, String>{};
|
||||
+ computeToPlainTextV2(
|
||||
+ buffer,
|
||||
+ map,
|
||||
+ includeSemanticsLabels: includeSemanticsLabels,
|
||||
+ includePlaceholders: includePlaceholders,
|
||||
+ );
|
||||
+ return (buffer.toString(), map);
|
||||
+ }
|
||||
+
|
||||
/// Flattens the [InlineSpan] tree to a list of
|
||||
/// [InlineSpanSemanticsInformation] objects.
|
||||
///
|
||||
@@ -358,6 +373,14 @@ abstract class InlineSpan extends DiagnosticableTree {
|
||||
bool includePlaceholders = true,
|
||||
});
|
||||
|
||||
+ @protected
|
||||
+ void computeToPlainTextV2(
|
||||
+ StringBuffer buffer,
|
||||
+ Map<int, String> map, {
|
||||
+ bool includeSemanticsLabels = true,
|
||||
+ bool includePlaceholders = true,
|
||||
+ });
|
||||
+
|
||||
/// Returns the UTF-16 code unit at the given `index` in the flattened string.
|
||||
///
|
||||
/// This only accounts for the [TextSpan.text] values and ignores [PlaceholderSpan]s.
|
||||
diff --git a/packages/flutter/lib/src/painting/placeholder_span.dart b/packages/flutter/lib/src/painting/placeholder_span.dart
|
||||
index 110ff860310..7ad808d05bc 100644
|
||||
--- a/packages/flutter/lib/src/painting/placeholder_span.dart
|
||||
+++ b/packages/flutter/lib/src/painting/placeholder_span.dart
|
||||
@@ -45,6 +45,7 @@ abstract class PlaceholderSpan extends InlineSpan {
|
||||
this.alignment = ui.PlaceholderAlignment.bottom,
|
||||
this.baseline,
|
||||
super.style,
|
||||
+ this.rawText,
|
||||
});
|
||||
|
||||
/// The unicode character to represent a placeholder.
|
||||
@@ -61,6 +62,8 @@ abstract class PlaceholderSpan extends InlineSpan {
|
||||
/// This is ignored when using other alignment modes.
|
||||
final TextBaseline? baseline;
|
||||
|
||||
+ final String? rawText;
|
||||
+
|
||||
/// [PlaceholderSpan]s are flattened to a `0xFFFC` object replacement character in the
|
||||
/// plain text representation when `includePlaceholders` is true.
|
||||
@override
|
||||
@@ -74,6 +77,21 @@ abstract class PlaceholderSpan extends InlineSpan {
|
||||
}
|
||||
}
|
||||
|
||||
+ @override
|
||||
+ void computeToPlainTextV2(
|
||||
+ StringBuffer buffer,
|
||||
+ Map<int, String> map, {
|
||||
+ bool includeSemanticsLabels = true,
|
||||
+ bool includePlaceholders = true,
|
||||
+ }) {
|
||||
+ if (includePlaceholders) {
|
||||
+ if (rawText != null) {
|
||||
+ map[buffer.length] = rawText!;
|
||||
+ }
|
||||
+ buffer.writeCharCode(placeholderCodeUnit);
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
@override
|
||||
void computeSemanticsInformation(List<InlineSpanSemanticsInformation> collector) {
|
||||
collector.add(InlineSpanSemanticsInformation.placeholder);
|
||||
diff --git a/packages/flutter/lib/src/painting/text_span.dart b/packages/flutter/lib/src/painting/text_span.dart
|
||||
index 09fba7fc4fb..57ee469358e 100644
|
||||
--- a/packages/flutter/lib/src/painting/text_span.dart
|
||||
+++ b/packages/flutter/lib/src/painting/text_span.dart
|
||||
@@ -398,6 +398,31 @@ class TextSpan extends InlineSpan implements HitTestTarget, MouseTrackerAnnotati
|
||||
}
|
||||
}
|
||||
|
||||
+ @override
|
||||
+ void computeToPlainTextV2(
|
||||
+ StringBuffer buffer,
|
||||
+ Map<int, String> map, {
|
||||
+ bool includeSemanticsLabels = true,
|
||||
+ bool includePlaceholders = true,
|
||||
+ }) {
|
||||
+ assert(debugAssertIsValid());
|
||||
+ if (semanticsLabel != null && includeSemanticsLabels) {
|
||||
+ buffer.write(semanticsLabel);
|
||||
+ } else if (text != null) {
|
||||
+ buffer.write(text);
|
||||
+ }
|
||||
+ if (children != null) {
|
||||
+ for (final InlineSpan child in children!) {
|
||||
+ child.computeToPlainTextV2(
|
||||
+ buffer,
|
||||
+ map,
|
||||
+ includeSemanticsLabels: includeSemanticsLabels,
|
||||
+ includePlaceholders: includePlaceholders,
|
||||
+ );
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
@override
|
||||
void computeSemanticsInformation(
|
||||
List<InlineSpanSemanticsInformation> collector, {
|
||||
diff --git a/packages/flutter/lib/src/rendering/paragraph.dart b/packages/flutter/lib/src/rendering/paragraph.dart
|
||||
index 97a41dafb8d..00672540c54 100644
|
||||
--- a/packages/flutter/lib/src/rendering/paragraph.dart
|
||||
+++ b/packages/flutter/lib/src/rendering/paragraph.dart
|
||||
@@ -502,13 +502,16 @@ class RenderParagraph extends RenderBox
|
||||
}
|
||||
|
||||
List<_SelectableFragment> _getSelectableFragments() {
|
||||
- final String plainText = text.toPlainText(includeSemanticsLabels: false);
|
||||
+ final (String plainText, Map<int, String> placeHolder) = text.toPlainTextV2(
|
||||
+ includeSemanticsLabels: false,
|
||||
+ );
|
||||
final result = <_SelectableFragment>[];
|
||||
var start = 0;
|
||||
while (start < plainText.length) {
|
||||
int end = plainText.indexOf(_placeholderCharacter, start);
|
||||
+ final hasPlaceholder = end != -1;
|
||||
if (start != end) {
|
||||
- if (end == -1) {
|
||||
+ if (!hasPlaceholder) {
|
||||
end = plainText.length;
|
||||
}
|
||||
result.add(
|
||||
@@ -521,6 +524,18 @@ class RenderParagraph extends RenderBox
|
||||
start = end;
|
||||
}
|
||||
start += 1;
|
||||
+ if (hasPlaceholder) {
|
||||
+ if (placeHolder[end] case final rawText?) {
|
||||
+ result.add(
|
||||
+ _SelectableFragment(
|
||||
+ paragraph: this,
|
||||
+ range: TextRange(start: end, end: end + 1),
|
||||
+ fullText: plainText,
|
||||
+ rawText: rawText,
|
||||
+ ),
|
||||
+ );
|
||||
+ }
|
||||
+ }
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -1467,7 +1482,7 @@ class RenderParagraph extends RenderBox
|
||||
class _SelectableFragment
|
||||
with Selectable, Diagnosticable, ChangeNotifier
|
||||
implements TextLayoutMetrics {
|
||||
- _SelectableFragment({required this.paragraph, required this.fullText, required this.range})
|
||||
+ _SelectableFragment({required this.paragraph, required this.fullText, required this.range, this.rawText})
|
||||
: assert(range.isValid && !range.isCollapsed && range.isNormalized) {
|
||||
if (kFlutterMemoryAllocationsEnabled) {
|
||||
ChangeNotifier.maybeDispatchObjectCreation(this);
|
||||
@@ -1478,6 +1493,7 @@ class _SelectableFragment
|
||||
final TextRange range;
|
||||
final RenderParagraph paragraph;
|
||||
final String fullText;
|
||||
+ final String? rawText;
|
||||
|
||||
TextPosition? _textSelectionStart;
|
||||
TextPosition? _textSelectionEnd;
|
||||
@@ -1626,6 +1642,9 @@ class _SelectableFragment
|
||||
if (_textSelectionStart == null || _textSelectionEnd == null) {
|
||||
return null;
|
||||
}
|
||||
+ if (rawText != null) {
|
||||
+ return SelectedContent(plainText: rawText!);
|
||||
+ }
|
||||
final int start = math.min(_textSelectionStart!.offset, _textSelectionEnd!.offset);
|
||||
final int end = math.max(_textSelectionStart!.offset, _textSelectionEnd!.offset);
|
||||
return SelectedContent(plainText: fullText.substring(start, end));
|
||||
@@ -1934,9 +1953,16 @@ class _SelectableFragment
|
||||
direction: paragraph.textDirection,
|
||||
);
|
||||
|
||||
- final TextPosition position = _clampTextPosition(
|
||||
- paragraph.getPositionForOffset(adjustedOffset),
|
||||
- );
|
||||
+ final TextPosition position;
|
||||
+ if (rawText != null) {
|
||||
+ position = _clampTextPosition(
|
||||
+ adjustedOffset.dx * 2 > _rect.left + _rect.right
|
||||
+ ? TextPosition(offset: range.end)
|
||||
+ : TextPosition(offset: range.start),
|
||||
+ );
|
||||
+ } else {
|
||||
+ position = _clampTextPosition(paragraph.getPositionForOffset(adjustedOffset));
|
||||
+ }
|
||||
_setSelectionPosition(position, isEnd: isEnd);
|
||||
if (position.offset == range.end) {
|
||||
return SelectionResult.next;
|
||||
diff --git a/packages/flutter/lib/src/widgets/widget_span.dart b/packages/flutter/lib/src/widgets/widget_span.dart
|
||||
index 09da3b76525..4a83efb0392 100644
|
||||
--- a/packages/flutter/lib/src/widgets/widget_span.dart
|
||||
+++ b/packages/flutter/lib/src/widgets/widget_span.dart
|
||||
@@ -78,7 +78,7 @@ class WidgetSpan extends PlaceholderSpan {
|
||||
///
|
||||
/// A [TextStyle] may be provided with the [style] property, but only the
|
||||
/// decoration, foreground, background, and spacing options will be used.
|
||||
- const WidgetSpan({required this.child, super.alignment, super.baseline, super.style})
|
||||
+ const WidgetSpan({required this.child, super.alignment, super.baseline, super.style, super.rawText})
|
||||
: assert(
|
||||
baseline != null ||
|
||||
!(identical(alignment, ui.PlaceholderAlignment.aboveBaseline) ||
|
||||
Reference in New Issue
Block a user