Skip to content

Instantly share code, notes, and snippets.

@Takhion
Last active August 11, 2022 11:09
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Takhion/e48938fd865fc9be64ec28ad2f7063d0 to your computer and use it in GitHub Desktop.
Save Takhion/e48938fd865fc9be64ec28ad2f7063d0 to your computer and use it in GitHub Desktop.
[Flutter] TextSelectionLayout (see https://stackoverflow.com/a/46244263/1306903)
// animated GIF: https://i.stack.imgur.com/mHOIc.gif
// see https://stackoverflow.com/a/46244263/1306903
import 'package:flutter/material.dart';
import 'ink_text_selection_layout.dart';
class Demo extends StatelessWidget {
@override
Widget build(context) => inkTextSelectionLayout(
richTextBuilder: (context) => new RichText(
key: key,
text: new TextSpan(
text: 'HELLO THIS IS MY ',
style: DefaultTextStyle.of(context).style,
children: <TextSpan>[
new TextSpan(
text: 'LONG',
style: new TextStyle(fontWeight: FontWeight.bold)),
new TextSpan(text: ' SENTENCE'),
],
),
),
selections: <TextSelection>[
const TextSelection(
baseOffset: 17,
extentOffset: 21,
),
],
);
}
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'text_selection_layout.dart';
export 'text_selection_layout.dart';
TextSelectionLayout inkTextSelectionLayout({
Key key,
TextBuilder textBuilder,
RichTextBuilder richTextBuilder,
@required List<TextSelection> selections,
Rect rippleRectPadding,
BorderRadius rippleBorderRadius,
}) {
final padding = rippleRectPadding ?? new Rect.fromLTRB(8.0, 8.0, 8.0, 8.0);
final radius = rippleBorderRadius ?? kMaterialEdges[MaterialType.button];
final WidgetBuilder inkBuilder = (_) => new InkWell(
borderRadius: radius,
onTap: () => {},
);
final MergeTextBoxes mergeTextBoxes = padding == Rect.zero
? null
: (textBoxes) {
Rect rect = TextSelectionLayout.defaultMergeTextBoxes(textBoxes);
if (rect != Rect.zero) {
rect = new Rect.fromLTRB(
rect.left - padding.left,
rect.top - padding.top,
rect.right + padding.right,
rect.bottom + padding.bottom,
);
}
return rect;
};
final data = selections
.map((selection) => new TextSelectionLayoutData(
selection: selection,
builder: inkBuilder,
mergeTextBoxes: mergeTextBoxes,
))
.toList(growable: false);
return new TextSelectionLayout(
key: key,
textBuilder: textBuilder,
richTextBuilder: richTextBuilder,
data: data,
);
}
import 'package:flutter/material.dart';
class KeyWrapper extends StatelessWidget {
const KeyWrapper({Key key, this.child}) : super(key: key);
final Widget child;
@override
Widget build(BuildContext context) => child;
}
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/foundation.dart';
import 'dart:ui' show TextBox;
import 'dart:math';
import 'key_wrapper.dart';
typedef Text TextBuilder(BuildContext context);
typedef RichText RichTextBuilder(BuildContext context);
typedef Rect MergeTextBoxes(List<TextBox> textBoxes);
@immutable
class TextSelectionLayoutData {
/// See [TextSelectionLayout.defaultMergeTextBoxes] for the default
/// implementation of [mergeTextBoxes] (used in case it's `null`).
const TextSelectionLayoutData({
@required this.selection,
@required this.builder,
this.mergeTextBoxes,
});
final TextSelection selection;
final WidgetBuilder builder;
final MergeTextBoxes mergeTextBoxes;
}
/// Provides a convenient way to place widgets in front of sections of text.
class TextSelectionLayout extends StatelessWidget {
/// Must provide [data] and either [textBuilder] or [richTextBuilder].
TextSelectionLayout({
Key key,
TextBuilder textBuilder,
RichTextBuilder richTextBuilder,
@required this.data,
})
: assert((textBuilder != null) != (richTextBuilder != null)),
textBuilder = textBuilder ?? richTextBuilder,
super(key: key);
final WidgetBuilder textBuilder;
final List<TextSelectionLayoutData> data;
final GlobalKey _textKey = new GlobalKey();
/// Returns a [Rect] that wraps all the [textBoxes].
static Rect defaultMergeTextBoxes(List<TextBox> textBoxes) =>
textBoxes?.fold(
null,
(Rect previous, TextBox textBox) => new Rect.fromLTRB(
min(previous?.left ?? textBox.left, textBox.left),
min(previous?.top ?? textBox.top, textBox.top),
max(previous?.right ?? textBox.right, textBox.right),
max(previous?.bottom ?? textBox.bottom, textBox.bottom),
),
) ??
Rect.zero;
@override
Widget build(context) => new Stack(
children: <Widget>[
new KeyWrapper(
key: _textKey,
child: textBuilder(context),
),
new Positioned.fill(
child: new LayoutBuilder(
builder: (context, _) => new Stack(
children: _getChildren(context),
),
),
),
],
);
List<Positioned> _getChildren(BuildContext context) {
if (data == null) return null;
final RenderParagraph renderParagraph =
_textKey.currentContext.findRenderObject() as RenderParagraph;
return data.map((datum) {
final List<TextBox> textBoxes =
renderParagraph.getBoxesForSelection(datum.selection);
final textBoxesToRect = datum.mergeTextBoxes ?? defaultMergeTextBoxes;
final rect = textBoxesToRect(textBoxes) ?? Rect.zero;
return new Positioned.fromRect(
rect: rect,
child: datum.builder(context),
);
}).toList(growable: false);
}
}
@droidery
Copy link

droidery commented Sep 20, 2018

@Takhion What license is this under? Thxs

@rishabh-chimple
Copy link

@Takhion did your code work for this issue also [https://github.com/flutter/flutter/issues/26581]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment