├── .gitignore ├── .metadata ├── CHANGELOG.md ├── LICENSE ├── README.md ├── example ├── example.dart └── pubspec.yaml ├── flutter_selectable_text.iml ├── lib └── flutter_selectable_text.dart ├── pubspec.yaml └── test └── flutter_selectable_text_test.dart /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .dart_tool/ 3 | .idea 4 | .packages 5 | .pub/ 6 | 7 | pubspec.lock 8 | 9 | build/ 10 | ios/ 11 | android/ 12 | 13 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 8661d8aecd626f7f57ccbcb735553edc05a2e713 8 | channel: stable 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.3.1 2 | * Fixed an assertion that was happening 3 | 4 | ## 0.3.0 5 | * Added the ability to remove the Cut and Paste options in the overlay toolbar 6 | * Added support for Flutter >=1.7.8 7 | * Updated flutter version dependency to >=1.7.8 <2.0.0. 8 | 9 | ## 0.2.4 10 | * Added support for Flutter >=1.7.0 11 | * Updated flutter version dependency to >=1.7.0 <2.0.0. 12 | 13 | ## 0.2.3 14 | * Added support for Flutter >=1.6.0 15 | * Updated flutter version dependency to >=1.6.0 <2.0.0. 16 | 17 | ## 0.2.2 18 | * Updated the description in pubspec.yaml 19 | 20 | ## 0.2.1 21 | * Updated the description in pubspec.yaml 22 | 23 | ## 0.2.0 24 | * Added support for Flutter >=1.4.7 25 | * Updated flutter version dependency to >=1.4.7 <2.0.0. 26 | 27 | ## 0.1.0 28 | * Updated to version 0.1.0. 29 | * Made strickt dependency on flutter version 1.2.1 because it is only compatible with that version. 30 | * Added an example 31 | 32 | ## 0.0.2 33 | * Updated the Flutter dependency 34 | 35 | ## 0.0.1 36 | 37 | * First published version 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google, Inc. All rights reserved. 2 | // 3 | // Redistribution and use in source and binary forms, with or without 4 | // modification, are permitted provided that the following conditions are 5 | // met: 6 | // 7 | // * Redistributions of source code must retain the above copyright 8 | // notice, this list of conditions and the following disclaimer. 9 | // * Redistributions in binary form must reproduce the above 10 | // copyright notice, this list of conditions and the following disclaimer 11 | // in the documentation and/or other materials provided with the 12 | // distribution. 13 | // * Neither the name of Google Inc. nor the names of its 14 | // contributors may be used to endorse or promote products derived from 15 | // this software without specific prior written permission. 16 | // 17 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Selectable Text 2 | 3 | A widget that allows to display text like the Text widget, while also allowing actions like selecting and copying text. 4 | 5 | Because this widget uses the EditableText widget, it does not support RichText nor text spans. 6 | 7 | This will work with different text styles and line heights. 8 | 9 | ## Getting Started 10 | 11 | Using the SelectableText widget is simple, just pass in the text: 12 | 13 | SelectableText("my text"); 14 | 15 | 16 | -------------------------------------------------------------------------------- /example/example.dart: -------------------------------------------------------------------------------- 1 | 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_selectable_text/flutter_selectable_text.dart'; 5 | 6 | void main() { 7 | runApp(new DemoApp()); 8 | } 9 | 10 | class DemoApp extends StatelessWidget { 11 | @override 12 | Widget build(BuildContext context) { 13 | return MaterialApp( 14 | title: 'Selectable Test Demo', 15 | theme: ThemeData( 16 | primarySwatch: Colors.grey, 17 | ), 18 | home: const SelectableTextDemo(), 19 | ); 20 | } 21 | } 22 | 23 | class SelectableTextDemo extends StatefulWidget { 24 | const SelectableTextDemo({Key key}) : super(key: key); 25 | 26 | @override 27 | _SelectableTextDemoState createState() => _SelectableTextDemoState(); 28 | } 29 | 30 | class _SelectableTextDemoState extends State { 31 | 32 | @override 33 | void initState() { 34 | super.initState(); 35 | } 36 | 37 | @override 38 | Widget build(BuildContext context) { 39 | return Scaffold( 40 | appBar: AppBar( 41 | title: const Text('Selectable Text Demo'), 42 | ), 43 | body: Center( 44 | child: Column( 45 | mainAxisAlignment: MainAxisAlignment.center, 46 | mainAxisSize: MainAxisSize.min, 47 | crossAxisAlignment: CrossAxisAlignment.stretch, 48 | children: [ 49 | SelectableText( 50 | "Select any part of this text!", 51 | textAlign: TextAlign.start, 52 | ) 53 | ], 54 | ), 55 | ), 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_selectable_text_example 2 | description: Demonstrates how to use the flutter_selectable_text plugin. 3 | 4 | dependencies: 5 | flutter: 6 | sdk: flutter 7 | 8 | dev_dependencies: 9 | flutter_test: 10 | sdk: flutter 11 | 12 | flutter_selectable_text: 13 | path: ../ 14 | 15 | # For information on the generic Dart part of this file, see the 16 | # following page: https://www.dartlang.org/tools/pub/pubspec 17 | 18 | # The following section is specific to Flutter. 19 | flutter: 20 | 21 | 22 | -------------------------------------------------------------------------------- /flutter_selectable_text.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /lib/flutter_selectable_text.dart: -------------------------------------------------------------------------------- 1 | library flutter_selectable_text; 2 | 3 | import 'package:flutter/cupertino.dart'; 4 | import 'package:flutter/gestures.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter/rendering.dart'; 7 | 8 | /// InputlessFocusNode is a FocusNode that does not consume the keyboard token, 9 | /// thereby preventing the keyboard from coming up when the node is focused 10 | class InputlessFocusNode extends FocusNode { 11 | 12 | // this is a special override needed, because the EditableText class creates 13 | // a TextInputConnection if the node has focus to force the keyboard to come up 14 | // this override will cause our FocusNode to pretend it doesn't have focus 15 | // when needed 16 | bool _overrideFocus; 17 | 18 | @override 19 | bool get hasFocus => _overrideFocus ?? super.hasFocus; 20 | 21 | @override 22 | bool consumeKeyboardToken() { 23 | return false; 24 | } 25 | } 26 | 27 | /// SelectableText widget 28 | /// It allows to display text given the style, text alignment, and text direction 29 | /// It will also allow the user to select text, and stop that selection by tapping anywhere 30 | /// on the text widget 31 | /// It will also allow to copy the text, and unfortunately, the Paste action will also appear 32 | /// but will be a no-op 33 | class SelectableText extends StatefulWidget { 34 | 35 | const SelectableText(this.text, { 36 | Key key, 37 | this.focusNode, 38 | this.style, 39 | this.textAlign = TextAlign.start, 40 | this.textDirection, 41 | this.cursorRadius, 42 | this.cursorColor, 43 | this.dragStartBehavior = DragStartBehavior.down, 44 | this.enableInteractiveSelection = true, 45 | this.onTap 46 | }) : super(key: key); 47 | 48 | final String text; 49 | final InputlessFocusNode focusNode; 50 | final TextStyle style; 51 | final TextAlign textAlign; 52 | final TextDirection textDirection; 53 | final Radius cursorRadius; 54 | final Color cursorColor; 55 | final bool enableInteractiveSelection; 56 | final DragStartBehavior dragStartBehavior; 57 | final GestureTapCallback onTap; 58 | 59 | _SelectableTextState createState() => _SelectableTextState(); 60 | } 61 | 62 | class _SelectableTextState extends State { 63 | 64 | final GlobalKey _editableTextKey = GlobalKey(); 65 | 66 | TextEditingController _controller; 67 | 68 | InputlessFocusNode _focusNode; 69 | InputlessFocusNode get _effectiveFocusNode => widget.focusNode ?? (_focusNode ??= InputlessFocusNode()); 70 | 71 | @override 72 | void initState() { 73 | super.initState(); 74 | 75 | _controller = TextEditingController(text: widget.text); 76 | } 77 | 78 | @override 79 | void didUpdateWidget(SelectableText oldWidget) { 80 | 81 | super.didUpdateWidget(oldWidget); 82 | } 83 | 84 | @override 85 | void dispose() { 86 | super.dispose(); 87 | } 88 | 89 | RenderEditable get _renderEditable => _editableTextKey.currentState.renderEditable; 90 | 91 | void _handleTapDown(TapDownDetails details) { 92 | _renderEditable.handleTapDown(details); 93 | } 94 | 95 | void _handleSingleTapUp(TapUpDetails details) { 96 | _effectiveFocusNode.unfocus(); 97 | if (widget.onTap != null) { 98 | widget.onTap(); 99 | } 100 | } 101 | 102 | void _handleSingleLongTapStart(LongPressStartDetails details) { 103 | // the EditableText widget will force the keyboard to come up if our focus node 104 | // is already focused. It does this by using a TextInputConnection 105 | // In order to tool it not to do that, we override our focus while selecting text 106 | _effectiveFocusNode._overrideFocus = false; 107 | 108 | switch (Theme.of(context).platform) { 109 | case TargetPlatform.iOS: 110 | _renderEditable.selectPositionAt( 111 | from: details.globalPosition, 112 | cause: SelectionChangedCause.longPress, 113 | ); 114 | break; 115 | case TargetPlatform.android: 116 | case TargetPlatform.fuchsia: 117 | _renderEditable.selectWord(cause: SelectionChangedCause.longPress); 118 | Feedback.forLongPress(context); 119 | break; 120 | } 121 | 122 | // Stop overriding our focus 123 | _effectiveFocusNode._overrideFocus = null; 124 | } 125 | 126 | void _handleSingleLongTapMoveUpdate(LongPressMoveUpdateDetails details) { 127 | // the EditableText widget will force the keyboard to come up if our focus node 128 | // is already focused. It does this by using a TextInputConnection 129 | // In order to tool it not to do that, we override our focus while selecting text 130 | _effectiveFocusNode._overrideFocus = false; 131 | 132 | _renderEditable.selectWordsInRange( 133 | from: details.globalPosition - details.offsetFromOrigin, 134 | to: details.globalPosition, 135 | cause: SelectionChangedCause.longPress, 136 | ); 137 | 138 | //Stop overriding our focus 139 | _effectiveFocusNode._overrideFocus = null; 140 | } 141 | 142 | void _handleSingleLongTapEnd(LongPressEndDetails details) { 143 | _editableTextKey.currentState.showToolbar(); 144 | } 145 | 146 | void _handleDoubleTapDown(TapDownDetails details) { 147 | _renderEditable.selectWord(cause: SelectionChangedCause.doubleTap); 148 | _editableTextKey.currentState.showToolbar(); 149 | } 150 | 151 | void _handleSelectionChanged(TextSelection selection, SelectionChangedCause cause) { 152 | // iOS cursor doesn't move via a selection handle. The scroll happens 153 | // directly from new text selection changes. 154 | if (Theme.of(context).platform == TargetPlatform.iOS 155 | && cause == SelectionChangedCause.longPress) { 156 | _editableTextKey.currentState?.bringIntoView(selection.base); 157 | } 158 | } 159 | 160 | @override 161 | Widget build(BuildContext context) { 162 | assert(debugCheckHasMaterial(context)); 163 | // TODO(jonahwilliams): uncomment out this check once we have migrated tests. 164 | // assert(debugCheckHasMaterialLocalizations(context)); 165 | assert(debugCheckHasDirectionality(context)); 166 | assert( 167 | !(widget.style != null && widget.style.inherit == false && 168 | (widget.style.fontSize == null || widget.style.textBaseline == null)), 169 | 'inherit false style must supply fontSize and textBaseline', 170 | ); 171 | 172 | final ThemeData themeData = Theme.of(context); 173 | final TextStyle style = themeData.textTheme.subhead.merge(widget.style); 174 | final FocusNode focusNode = _effectiveFocusNode; 175 | 176 | TextSelectionControls textSelectionControls; 177 | bool paintCursorAboveText; 178 | bool cursorOpacityAnimates; 179 | Offset cursorOffset; 180 | Color cursorColor = widget.cursorColor; 181 | Radius cursorRadius = widget.cursorRadius; 182 | 183 | switch (themeData.platform) { 184 | case TargetPlatform.iOS: 185 | textSelectionControls = _TextSelectionControls(cupertinoTextSelectionControls); 186 | paintCursorAboveText = true; 187 | cursorOpacityAnimates = true; 188 | cursorColor ??= CupertinoTheme.of(context).primaryColor; 189 | cursorRadius ??= const Radius.circular(2.0); 190 | // An eyeballed value that moves the cursor slightly left of where it is 191 | // rendered for text on Android so its positioning more accurately matches the 192 | // native iOS text cursor positioning. 193 | // 194 | // This value is in device pixels, not logical pixels as is typically used 195 | // throughout the codebase. 196 | const int _iOSHorizontalOffset = -2; 197 | cursorOffset = Offset(_iOSHorizontalOffset / MediaQuery.of(context).devicePixelRatio, 0); 198 | break; 199 | 200 | case TargetPlatform.android: 201 | case TargetPlatform.fuchsia: 202 | textSelectionControls = _TextSelectionControls(materialTextSelectionControls); 203 | paintCursorAboveText = false; 204 | cursorOpacityAnimates = false; 205 | cursorColor ??= themeData.cursorColor; 206 | break; 207 | } 208 | 209 | Widget child = RepaintBoundary( 210 | child: _EditableText( 211 | key: _editableTextKey, 212 | controller: _controller, 213 | focusNode: focusNode, 214 | style: style, 215 | textAlign: widget.textAlign, 216 | textDirection: widget.textDirection, 217 | maxLines: null, 218 | selectionColor: themeData.textSelectionColor, 219 | selectionControls: widget.enableInteractiveSelection ? textSelectionControls : null, 220 | onSelectionChanged: _handleSelectionChanged, 221 | rendererIgnoresPointer: true, 222 | cursorWidth: 0, 223 | cursorRadius: cursorRadius, 224 | cursorColor: cursorColor, 225 | cursorOpacityAnimates: cursorOpacityAnimates, 226 | cursorOffset: cursorOffset, 227 | paintCursorAboveText: paintCursorAboveText, 228 | backgroundCursorColor: CupertinoColors.inactiveGray, 229 | enableInteractiveSelection: widget.enableInteractiveSelection, 230 | dragStartBehavior: widget.dragStartBehavior, 231 | ), 232 | ); 233 | 234 | return Semantics( 235 | child: TextSelectionGestureDetector( 236 | onTapDown: _handleTapDown, 237 | onSingleTapUp: _handleSingleTapUp, 238 | onSingleLongTapStart: _handleSingleLongTapStart, 239 | onSingleLongTapMoveUpdate: _handleSingleLongTapMoveUpdate, 240 | onSingleLongTapEnd: _handleSingleLongTapEnd, 241 | onDoubleTapDown: _handleDoubleTapDown, 242 | behavior: HitTestBehavior.translucent, 243 | child: child, 244 | ), 245 | ); 246 | } 247 | } 248 | 249 | /// The _EditableText class extends the [EditableText] class because 250 | /// for two reasons 251 | /// 1) The [EditableText] widget adds a [Scrollable] widget in the subtree 252 | /// so that it can scroll the text if needed (remember, it's supposed to be an input field) 253 | /// This doesn't seem to cause any problems when the TextStyle's line height 254 | /// is set to 1.0, but when it's greater, the text springs up and down when selecting 255 | /// text. We actually remove the [Scrollable] from the subtree, and instead, just 256 | /// place it in a Column widget (because a scroll controller still exists in the [Editabletext] 257 | /// so we need it to be attached to an actual [Scrollable]. 258 | /// Then, we create our own FakeRenderBox so that it can set the viewport to 0.0 259 | /// on the [Scrollable] 260 | /// 2) When the selection toolbar does a copy or paste operation, it then calls 261 | /// hideToolbar() on the EditableTextState, but that method doesn't unfocus our node 262 | /// so we do 263 | class _EditableText extends EditableText { 264 | 265 | _EditableText({ 266 | Key key, 267 | @required TextEditingController controller, 268 | @required FocusNode focusNode, 269 | @required TextStyle style, 270 | @required Color cursorColor, 271 | @required Color backgroundCursorColor, 272 | TextAlign textAlign = TextAlign.start, 273 | TextDirection textDirection, 274 | int maxLines = 1, 275 | Color selectionColor, 276 | TextSelectionControls selectionControls, 277 | SelectionChangedCallback onSelectionChanged, 278 | bool rendererIgnoresPointer = false, 279 | double cursorWidth = 2.0, 280 | Radius cursorRadius, 281 | bool cursorOpacityAnimates = false, 282 | Offset cursorOffset, 283 | bool paintCursorAboveText = false, 284 | DragStartBehavior dragStartBehavior = DragStartBehavior.down, 285 | bool enableInteractiveSelection, 286 | }) : super( 287 | key: key, 288 | controller: controller, 289 | focusNode: focusNode, 290 | style: style, 291 | cursorColor: cursorColor, 292 | backgroundCursorColor: backgroundCursorColor, 293 | textAlign: textAlign, 294 | textDirection: textDirection, 295 | maxLines: maxLines, 296 | selectionColor: selectionColor, 297 | selectionControls: selectionControls, 298 | onSelectionChanged: onSelectionChanged, 299 | rendererIgnoresPointer: rendererIgnoresPointer, 300 | cursorWidth: cursorWidth, 301 | cursorRadius: cursorRadius, 302 | cursorOpacityAnimates: cursorOpacityAnimates, 303 | cursorOffset: cursorOffset, 304 | paintCursorAboveText: paintCursorAboveText, 305 | dragStartBehavior: dragStartBehavior, 306 | enableInteractiveSelection: enableInteractiveSelection, 307 | ); 308 | 309 | _EditableTextState createState() => _EditableTextState(); 310 | } 311 | 312 | class _EditableTextState extends EditableTextState { 313 | 314 | @override 315 | void hideToolbar() { 316 | // unfocus our node instead of just hiding the toolbar because we don't 317 | // want to keep focus anymore 318 | widget.focusNode.unfocus(); 319 | } 320 | 321 | @override 322 | Widget build(BuildContext context) { 323 | Widget widget = super.build(context); 324 | assert(widget is Scrollable); 325 | Scrollable scrollable = widget; 326 | return Column( 327 | mainAxisSize: MainAxisSize.min, 328 | children: [ 329 | Scrollable( 330 | excludeFromSemantics: true, 331 | axisDirection: AxisDirection.right, 332 | controller: scrollable.controller, 333 | physics: const NeverScrollableScrollPhysics(), 334 | dragStartBehavior: scrollable.dragStartBehavior, 335 | viewportBuilder: (context, offset) { 336 | // create a _FakeRenderObject so that it can safely set 337 | // a viewport of 0.0 on the Scrollable so that everything is 338 | // happy 339 | return _FakeRenderObject(offset: offset); 340 | } 341 | ), 342 | scrollable.viewportBuilder(context, ViewportOffset.zero()) 343 | ], 344 | ); 345 | } 346 | } 347 | 348 | /// FakeRenderObject 349 | class _FakeRenderObject extends LeafRenderObjectWidget { 350 | 351 | _FakeRenderObject({@required this.offset}); 352 | 353 | final ViewportOffset offset; 354 | 355 | @override 356 | RenderObject createRenderObject(BuildContext context) { 357 | return _FakeRenderBox(offset: offset); 358 | } 359 | 360 | @override 361 | void updateRenderObject(BuildContext context, _FakeRenderBox renderObject) { 362 | renderObject 363 | ..offset = offset; 364 | } 365 | } 366 | 367 | /// FakeRenderBox 368 | class _FakeRenderBox extends RenderBox { 369 | 370 | _FakeRenderBox({ 371 | @required ViewportOffset offset 372 | }) : assert(offset != null), 373 | _offset = offset; 374 | 375 | ViewportOffset get offset => _offset; 376 | ViewportOffset _offset; 377 | set offset(ViewportOffset value) { 378 | assert(value != null); 379 | if (_offset == value) 380 | return; 381 | if (attached) 382 | _offset.removeListener(markNeedsPaint); 383 | _offset = value; 384 | if (attached) 385 | _offset.addListener(markNeedsPaint); 386 | markNeedsLayout(); 387 | } 388 | 389 | @override 390 | void attach(PipelineOwner owner) { 391 | super.attach(owner); 392 | _offset.addListener(markNeedsPaint); 393 | } 394 | 395 | @override 396 | void detach() { 397 | _offset.removeListener(markNeedsPaint); 398 | super.detach(); 399 | } 400 | 401 | @override 402 | void performLayout() { 403 | assert(_offset != null); 404 | size = Size(constraints.minWidth, constraints.minHeight); 405 | _offset.applyViewportDimension(0.0); 406 | _offset.applyContentDimensions(0.0, 0.0); 407 | } 408 | } 409 | 410 | /// _TextSelectionDelegateHelper is used to ensure the Cut option in the toolbar 411 | /// doesn't show, and a Paste operation does nothing 412 | class _TextSelectionDelegateHelper extends TextSelectionDelegate { 413 | 414 | _TextSelectionDelegateHelper(this.delegate); 415 | 416 | final TextSelectionDelegate delegate; 417 | 418 | // we don't allow to cut 419 | bool get cutEnabled => false; 420 | 421 | // we allow to copy 422 | bool get copyEnabled => true; 423 | 424 | // we don't allow to paste 425 | bool get pasteEnabled => false; 426 | 427 | // well allow to select all 428 | bool get selectAllEnabled => true; 429 | 430 | TextEditingValue get textEditingValue { 431 | return delegate.textEditingValue; 432 | } 433 | 434 | set textEditingValue(TextEditingValue value) { 435 | // seeing we don't allow to paste or cut, let's make sure the text doesn't get changed 436 | delegate.textEditingValue = value.copyWith(text: delegate.textEditingValue.text); 437 | } 438 | 439 | void hideToolbar() { 440 | delegate.hideToolbar(); 441 | } 442 | 443 | void bringIntoView(TextPosition position) { 444 | delegate.bringIntoView(position); 445 | } 446 | } 447 | 448 | 449 | /// _TextSelectionControls just wraps the platform specific controls object 450 | /// and passes it our own _TextSelectionDelegateHelper delegate 451 | class _TextSelectionControls extends TextSelectionControls { 452 | 453 | _TextSelectionControls(this._platformTextSelectionControls); 454 | 455 | final TextSelectionControls _platformTextSelectionControls; 456 | 457 | /// Builder for iOS-style copy/paste text selection toolbar. 458 | @override 459 | Widget buildToolbar( 460 | BuildContext context, 461 | Rect globalEditableRegion, 462 | double textLineHeight, 463 | Offset position, 464 | List endpoints, 465 | TextSelectionDelegate delegate, 466 | ) { 467 | return _platformTextSelectionControls.buildToolbar( 468 | context, 469 | globalEditableRegion, 470 | textLineHeight, 471 | position, 472 | endpoints, 473 | _TextSelectionDelegateHelper(delegate) 474 | ); 475 | } 476 | 477 | /// Builder for iOS text selection edges. 478 | @override 479 | Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textLineHeight) { 480 | return _platformTextSelectionControls.buildHandle(context, type, textLineHeight); 481 | } 482 | 483 | @override 484 | Offset getHandleAnchor(TextSelectionHandleType type, double textLineHeight) { 485 | return _platformTextSelectionControls.getHandleAnchor(type, textLineHeight); 486 | } 487 | 488 | @override 489 | Size getHandleSize(double textLineHeight) { 490 | return _platformTextSelectionControls.getHandleSize(textLineHeight); 491 | } 492 | } 493 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_selectable_text 2 | description: A widget that allows to display text like the Text widget, while also allowing actions like selecting and copying text. 3 | version: 0.3.1 4 | author: Danny Valente 5 | homepage: https://github.com/dannyvalente/flutter_selectable_text 6 | 7 | environment: 8 | sdk: ">=2.1.0 <3.0.0" 9 | flutter: ">=1.7.8 <2.0.0" 10 | 11 | dependencies: 12 | flutter: 13 | sdk: flutter 14 | 15 | dev_dependencies: 16 | flutter_test: 17 | sdk: flutter 18 | 19 | # For information on the generic Dart part of this file, see the 20 | # following page: https://www.dartlang.org/tools/pub/pubspec 21 | 22 | # The following section is specific to Flutter. 23 | flutter: 24 | 25 | 26 | -------------------------------------------------------------------------------- /test/flutter_selectable_text_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | 3 | import 'package:flutter_selectable_text/flutter_selectable_text.dart'; 4 | 5 | void main() { 6 | 7 | } 8 | --------------------------------------------------------------------------------