import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'basic_text_input_client.dart'; /// A basic text field. Defines the appearance of a basic text input client. class BasicTextField extends StatefulWidget { const BasicTextField({ super.key, required this.controller, required this.style, required this.focusNode, this.contextMenuBuilder = _defaultContextMenuBuilder, }); final TextEditingController controller; final TextStyle style; final FocusNode focusNode; final BasicTextFieldContextMenuBuilder? contextMenuBuilder; static Widget _defaultContextMenuBuilder( BuildContext context, ClipboardStatus clipboardStatus, VoidCallback? onCopy, VoidCallback? onCut, VoidCallback? onPaste, VoidCallback? onSelectAll, TextSelectionToolbarAnchors anchors, ) { return AdaptiveTextSelectionToolbar.editable( clipboardStatus: clipboardStatus, onCopy: onCopy, onCut: onCut, onPaste: onPaste, onSelectAll: onSelectAll, anchors: anchors, ); } @override State createState() => _BasicTextFieldState(); } class _BasicTextFieldState extends State { final GlobalKey textInputClientKey = GlobalKey(); BasicTextInputClientState? get _textInputClient => textInputClientKey.currentState; RenderEditable get _renderEditable => _textInputClient!.renderEditable; // For text selection gestures. // The viewport offset pixels of the [RenderEditable] at the last drag start. double _dragStartViewportOffset = 0.0; late DragStartDetails _startDetails; // For text selection. TextSelectionControls? _textSelectionControls; bool _showSelectionHandles = false; bool _shouldShowSelectionHandles(SelectionChangedCause? cause) { // When the text field is activated by something that doesn't trigger the // selection overlay, we shouldn't show the handles either. if (cause == SelectionChangedCause.keyboard) { return false; } if (cause == SelectionChangedCause.longPress || cause == SelectionChangedCause.scribble) { return true; } if (widget.controller.text.isNotEmpty) { return true; } return false; } void _handleSelectionChanged( TextSelection selection, SelectionChangedCause? cause) { final bool willShowSelectionHandles = _shouldShowSelectionHandles(cause); if (willShowSelectionHandles != _showSelectionHandles) { setState(() { _showSelectionHandles = willShowSelectionHandles; }); } } void _onDragUpdate(DragUpdateDetails details) { final Offset startOffset = _renderEditable.maxLines == 1 ? Offset(_renderEditable.offset.pixels - _dragStartViewportOffset, 0.0) : Offset(0.0, _renderEditable.offset.pixels - _dragStartViewportOffset); _renderEditable.selectPositionAt( from: _startDetails.globalPosition - startOffset, to: details.globalPosition, cause: SelectionChangedCause.drag, ); } void _onDragStart(DragStartDetails details) { _startDetails = details; _dragStartViewportOffset = _renderEditable.offset.pixels; } @override Widget build(BuildContext context) { switch (Theme.of(this.context).platform) { // ignore: todo // TODO(Renzo-Olivares): Remove use of deprecated members once // TextSelectionControls.buildToolbar has been deleted. // See https://github.com/flutter/flutter/pull/124611 and // https://github.com/flutter/flutter/pull/124262 for more details. case TargetPlatform.iOS: // ignore: deprecated_member_use _textSelectionControls = cupertinoTextSelectionHandleControls; case TargetPlatform.macOS: // ignore: deprecated_member_use _textSelectionControls = cupertinoDesktopTextSelectionHandleControls; case TargetPlatform.android: case TargetPlatform.fuchsia: // ignore: deprecated_member_use _textSelectionControls = materialTextSelectionHandleControls; case TargetPlatform.linux: // ignore: deprecated_member_use _textSelectionControls = desktopTextSelectionHandleControls; case TargetPlatform.windows: // ignore: deprecated_member_use _textSelectionControls = desktopTextSelectionHandleControls; } return TextFieldTapRegion( child: GestureDetector( behavior: HitTestBehavior.translucent, onPanStart: (dragStartDetails) => _onDragStart(dragStartDetails), onPanUpdate: (dragUpdateDetails) => _onDragUpdate(dragUpdateDetails), onSecondaryTapDown: (secondaryTapDownDetails) { _renderEditable.selectWordsInRange( from: secondaryTapDownDetails.globalPosition, cause: SelectionChangedCause.tap); _renderEditable.handleSecondaryTapDown(secondaryTapDownDetails); _textInputClient!.hideToolbar(); _textInputClient!.showToolbar(); }, onTap: () { _textInputClient!.requestKeyboard(); _textInputClient!.hideToolbar(); }, onTapDown: (tapDownDetails) { _renderEditable.handleTapDown(tapDownDetails); _renderEditable.selectPosition(cause: SelectionChangedCause.tap); }, onLongPressMoveUpdate: (longPressMoveUpdateDetails) { switch (Theme.of(this.context).platform) { case TargetPlatform.iOS: case TargetPlatform.macOS: _renderEditable.selectPositionAt( from: longPressMoveUpdateDetails.globalPosition, cause: SelectionChangedCause.longPress, ); case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.linux: case TargetPlatform.windows: _renderEditable.selectWordsInRange( from: longPressMoveUpdateDetails.globalPosition - longPressMoveUpdateDetails.offsetFromOrigin, to: longPressMoveUpdateDetails.globalPosition, cause: SelectionChangedCause.longPress, ); } }, onLongPressEnd: (longPressEndDetails) => _textInputClient!.showToolbar(), onHorizontalDragStart: (dragStartDetails) => _onDragStart(dragStartDetails), onHorizontalDragUpdate: (dragUpdateDetails) => _onDragUpdate(dragUpdateDetails), child: SizedBox( height: double.infinity, width: MediaQuery.of(context).size.width, child: Container( decoration: BoxDecoration( border: Border.all(color: Colors.black), borderRadius: const BorderRadius.all(Radius.circular(4.0)), ), child: BasicTextInputClient( key: textInputClientKey, controller: widget.controller, style: widget.style, focusNode: widget.focusNode, selectionControls: _textSelectionControls, onSelectionChanged: _handleSelectionChanged, showSelectionHandles: _showSelectionHandles, contextMenuBuilder: widget.contextMenuBuilder, ), ), ), ), ); } }