selectable 0.3.3 copy "selectable: ^0.3.3" to clipboard
selectable: ^0.3.3 copied to clipboard

A Flutter widget that enables text selection over all the text widgets it contains.

example/lib/main.dart

import 'dart:async';
import 'dart:math' as math;

import 'package:float_column/float_column.dart';
import 'package:flutter/material.dart';
import 'package:selectable/selectable.dart';

// import 'my_selection_painter.dart';

// Allow print in the example app.
// ignore_for_file: avoid_print, unreachable_from_main

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Selectable Example',
      theme: ThemeData.light(),
      darkTheme: ThemeData.dark(),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

  @override
  // Stateful widgets with a private state class need this.
  // ignore: library_private_types_in_public_api
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final _scrollController = ScrollController();
  final _selectionController = SelectableController();
  var _isTextSelected = false;
  var _showSelection = true;
  late Timer _timer;
  List<InlineSpan> _spans = [_text];
  // List<InlineSpan> _spans = [const TextSpan(text: text1)];

  @override
  void initState() {
    super.initState();

    _selectionController
      // ..setCustomPainter(MySelectionPainter())
      // ..setCustomRectifier(SelectionRectifiers.merged)
      ..setCustomRectifier((rects) => rects
          .map((r) => Rect.fromLTRB(r.left - 2, r.top, r.right + 2, r.bottom))
          .toList())
      ..addListener(_selectionChangedListener);

    _timer =
        Timer.periodic(const Duration(seconds: 1), (_) => _selectRandomWord());
  }

  @override
  void dispose() {
    _timer.cancel();
    _selectionController
      ..removeListener(_selectionChangedListener)
      ..dispose();
    _scrollController.dispose();
    super.dispose();
  }

  void _selectionChangedListener() {
    // if (_isTextSelected != _selectionController.isTextSelected) {
    if (mounted) {
      setState(() {
        _isTextSelected = _selectionController.isTextSelected;
      });
    }
    // }
  }

  void _toggleShowHideSelection() {
    setState(() {
      _showSelection = !_showSelection;
      if (_showSelection) {
        _selectionController.unhide();
      } else {
        _selectionController.hide();
      }
    });
  }

  void _toggleRectifier() {
    setState(() {
      _selectionController.setCustomRectifier(
          _selectionController.getCustomRectifier() ==
                  SelectionRectifiers.identity
              ? SelectionRectifiers.merged
              : SelectionRectifiers.identity);
    });
  }

  void _selectRandomWord() {
    // final text = _selectionController.getContainedText();
    // if (text.isNotEmpty) {
    //   final i = random(max: text.length);
    //   if (_selectionController.selectWordAtIndex(i, key: 1)) {
    //     // print('selected word at $i');
    //   } else {
    //     // print('failed to select word at $i');
    //   }
    // }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: CustomScrollView(
        controller: _scrollController,
        slivers: [
          const SliverAppBar(
            pinned: true,
            collapsedHeight: kToolbarHeight,
            title: Text('Selectable Example'),
            expandedHeight: 70,
            // flexibleSpace: FlexibleSpaceBar(title: ),
          ),
          SliverList(
            delegate: SliverChildBuilderDelegate(
              (context, index) => Stack(
                children: [
                  Selectable(
                    selectWordOnDoubleTap: true,
                    topOverlayHeight:
                        kToolbarHeight + MediaQuery.paddingOf(context).top,
                    selectionController: _selectionController,
                    scrollController: _scrollController,
                    useExperimentalPopupMenu: false, // !kIsWeb,
                    // selectionColor: Colors.orange.withAlpha(75),
                    // showSelection: _showSelection,
                    popupMenuItems: [
                      const SelectableMenuItem(
                          type: SelectableMenuItemType.copy),
                      const SelectableMenuItem(
                          type: SelectableMenuItemType.define),
                      SelectableMenuItem(
                        icon: Icons.brush_outlined,
                        title: 'Color Red',
                        isEnabled: (controller) => controller!.isTextSelected,
                        handler: (controller) {
                          final selection = controller?.getSelection();
                          final startIndex = selection?.startIndex;
                          final endIndex = selection?.endIndex;
                          if (selection != null &&
                              startIndex != null &&
                              endIndex != null &&
                              endIndex > startIndex) {
                            // Split `_spans` at `startIndex`:
                            final result1 = _spans.splitAtCharacterIndex(
                                SplitAtIndex(startIndex));

                            // Split `result1.last` at `endIndex - startIndex`:
                            final result2 = result1.last.splitAtCharacterIndex(
                                SplitAtIndex(endIndex - startIndex));

                            // Update the state with the new spans.
                            setState(() {
                              _spans = [
                                if (result1.length > 1) ...result1.first,
                                TextSpan(
                                  children: result2.first,
                                  style: const TextStyle(color: Colors.red),
                                ),
                                if (result2.length > 1) ...result2.last,
                              ];
                            });

                            controller!.deselect();
                          }

                          return true;
                        },
                      ),
                    ],
                    child: Container(
                      padding: const EdgeInsets.all(20),
                      child:
                          FloatColumn(children: [TextSpan(children: _spans)]),
                    ),
                  ),
                  ..._selectionController.getSelection()?.rects?.map((r) {
                        return Positioned.fromRect(
                            rect: r,
                            child: InkWell(onTap: () => print('onTap: $r')));
                      }) ??
                      [],
                ],
              ),
              childCount: 1,
            ),
          ),
        ],
      ),
      floatingActionButton: _isTextSelected
          ? Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                FloatingActionButton.extended(
                  onPressed: _toggleShowHideSelection,
                  label: Text(
                      _showSelection ? 'hide selection' : 'show selection'),
                ),
                const SizedBox(height: 8),
                FloatingActionButton.extended(
                  onPressed: _toggleRectifier,
                  label: const Text('switch rectifier'),
                ),
              ],
            )
          : FloatingActionButton.extended(
              onPressed: _selectionController.selectAll,
              label: const Text('select all'),
            ),
    );
  }
}

// cspell: disable

final _text = TextSpan(children: [
  const TaggedTextSpan(
    tag: 'directions',
    text: 'Double-tap or long press on a word to select it, '
        'then drag the selection controls to change the '
        'selection.\n\n',
    style: _headlineStyle,
  ),
  WidgetSpan(
    child: Floatable(
      float: FCFloat.end,
      clear: FCClear.both,
      padding: const EdgeInsetsDirectional.only(start: 16),
      maxWidthPercentage: 0.333,
      child: Container(height: 150, color: Colors.orange),
    ),
  ),
  const TextSpan(
    text: 'Lorem ipsum dolor\n',
    style: _headlineStyle,
  ),
  const TextSpan(text: text1, style: _textStyle2),
  const TextSpan(text: '\n\n'),
  WidgetSpan(
    child: Floatable(
      float: FCFloat.start,
      clear: FCClear.both,
      padding: const EdgeInsetsDirectional.only(end: 16),
      maxWidthPercentage: 0.333,
      child: Container(height: 150, color: Colors.blue),
    ),
  ),
  const TextSpan(
    text: 'Excepteur sint occaecat\n',
    style: _headlineStyle,
  ),
  TextSpan(children: [_span1], style: _textStyle1),
]);

const text1 = 'Lorem ipsum dolor sit amet, consectetur—adipiscing elit, sed '
    'do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim '
    'ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut '
    'aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit '
    'in voluptate velit esse cillum dolore eu fugiat nulla pariatur. '
    'Excepteur sint occaecat cupidatat non proident, sunt in culpa qui '
    'officia deserunt mollit anim id est laborum.';

final _span1 = TextSpan(
  children: <InlineSpan>[
    const TextSpan(style: TextStyle(color: Colors.red), text: 'Lor'),
    const TextSpan(style: TextStyle(color: Colors.blue), text: 'em i'),
    const TextSpan(style: TextStyle(color: Colors.green), text: 'psu'),
    const TextSpan(style: TextStyle(color: Colors.red), text: 'm do'),
    const TextSpan(style: TextStyle(color: Colors.blue), text: 'lor'),
    const TextSpan(style: TextStyle(color: Colors.green), text: ' sit '),
    const TextSpan(style: TextStyle(color: Colors.red), text: 'ame'),
    const TextSpan(style: TextStyle(color: Colors.blue), text: 't, c'),
    const TextSpan(text: 'onsectetur '),
    WidgetSpan(child: Container(width: 20, height: 20, color: Colors.orange)),
    const TextSpan(text: ' adipiscing '),
    WidgetSpan(child: Container(width: 20, height: 20, color: Colors.green)),
    const TextSpan(
        text: ' elit, sed do eiusmod tempor incididunt '
            'ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis '
            'nostrud exercitation ullamco laboris nisi ut aliquip ex ea '
            'commodo consequat. Duis aute irure dolor in reprehenderit in '
            'voluptate velit esse cillum dolore eu fugiat nulla pariatur. '
            'Excepteur sint occaecat cupidatat non proident, sunt in culpa '
            'qui officia deserunt mollit anim id est laborum.'),
  ],
);

const TextStyle _textStyle1 = TextStyle(
  fontSize: 20,
  fontWeight: FontWeight.normal,
  height: 1.5,
);

const TextStyle _textStyle2 = TextStyle(
  fontSize: 20,
  fontWeight: FontWeight.normal,
  height: 1.5,
  // backgroundColor: Colors.transparent,
);

const TextStyle _headlineStyle = TextStyle(
  fontSize: 28,
  fontWeight: FontWeight.w600,
);

math.Random? _random;

/// Returns a random integer uniformly distributed in the range from [min],
/// inclusive, to [max], exclusive.
int random({int min = 0, required int max}) {
  assert(max > min);
  return (_random ??= math.Random()).nextInt(math.max(0, max - min)) + min;
}

/* showDialog<void>(
  context: context,
  barrierDismissible: true,
  builder: (builder) {
    return AlertDialog(
      contentPadding: EdgeInsets.zero,
      content: Container(
        padding: const EdgeInsets.all(16),
        child: Text(controller!.getSelection()!.text!),
      ),
      shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(8)),
    );
  },
); */
71
likes
150
points
2.84k
downloads

Publisher

verified publisherronbooth.com

Weekly Downloads

A Flutter widget that enables text selection over all the text widgets it contains.

Repository (GitHub)
View/report issues

Documentation

API reference

License

MIT (license)

Dependencies

characters, collection, equatable, float_column, flutter, url_launcher

More

Packages that depend on selectable