using System.Collections.Generic; using Unity.UIWidgets.foundation; using Unity.UIWidgets.gestures; using Unity.UIWidgets.painting; using Unity.UIWidgets.rendering; using Unity.UIWidgets.service; using Unity.UIWidgets.ui; using Unity.UIWidgets.widgets; using UnityEngine; using Color = Unity.UIWidgets.ui.Color; using TextStyle = Unity.UIWidgets.painting.TextStyle; namespace Unity.UIWidgets.material { public delegate Widget InputCounterWidgetBuilder( BuildContext buildContext, int? currentLength, int? maxLength, bool? isFocused); public class TextField : StatefulWidget { public TextField(Key key = null, TextEditingController controller = null, FocusNode focusNode = null, InputDecoration decoration = null, bool noDecoration = false, TextInputType keyboardType = null, TextInputAction? textInputAction = null, TextCapitalization textCapitalization = TextCapitalization.none, TextStyle style = null, TextAlign textAlign = TextAlign.left, TextDirection textDirection = TextDirection.ltr, bool autofocus = false, bool obscureText = false, bool autocorrect = false, int? maxLines = 1, int? maxLength = null, bool maxLengthEnforced = true, ValueChanged onChanged = null, VoidCallback onEditingComplete = null, ValueChanged onSubmitted = null, List inputFormatters = null, bool? enabled = null, float? cursorWidth = 2.0f, Radius cursorRadius = null, Color cursorColor = null, Brightness? keyboardAppearance = null, EdgeInsets scrollPadding = null, DragStartBehavior dragStartBehavior = DragStartBehavior.down, bool? enableInteractiveSelection = null, GestureTapCallback onTap = null, InputCounterWidgetBuilder buildCounter = null ) : base(key: key) { D.assert(maxLines == null || maxLines > 0); D.assert(maxLength == null || maxLength == TextField.noMaxLength || maxLength > 0); this.controller = controller; this.focusNode = focusNode; this.decoration = noDecoration ? null : (decoration ?? new InputDecoration()); this.textInputAction = textInputAction; this.textCapitalization = textCapitalization; this.style = style; this.textAlign = textAlign; this.textDirection = textDirection; this.autofocus = autofocus; this.obscureText = obscureText; this.autocorrect = autocorrect; this.maxLines = maxLines; this.maxLength = maxLength; this.maxLengthEnforced = maxLengthEnforced; this.onChanged = onChanged; this.onEditingComplete = onEditingComplete; this.onSubmitted = onSubmitted; this.inputFormatters = inputFormatters; this.enabled = enabled; this.cursorWidth = cursorWidth; this.cursorColor = cursorColor; this.cursorRadius = cursorRadius; this.onSubmitted = onSubmitted; this.keyboardAppearance = keyboardAppearance; this.enableInteractiveSelection = enableInteractiveSelection; this.onTap = onTap; this.keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline); this.scrollPadding = scrollPadding ?? EdgeInsets.all(20.0f); this.dragStartBehavior = dragStartBehavior; this.buildCounter = buildCounter; } public readonly TextEditingController controller; public readonly FocusNode focusNode; public readonly InputDecoration decoration; public readonly TextInputType keyboardType; public readonly TextInputAction? textInputAction; public readonly TextCapitalization textCapitalization; public readonly TextStyle style; public readonly TextAlign textAlign; public readonly TextDirection textDirection; public readonly bool autofocus; public readonly bool obscureText; public readonly bool autocorrect; public readonly int? maxLines; public const long noMaxLength = -1; public readonly int? maxLength; public readonly bool maxLengthEnforced; public readonly ValueChanged onChanged; public readonly VoidCallback onEditingComplete; public readonly ValueChanged onSubmitted; public readonly List inputFormatters; public readonly bool? enabled; public readonly float? cursorWidth; public readonly Radius cursorRadius; public readonly Color cursorColor; public readonly Brightness? keyboardAppearance; public readonly EdgeInsets scrollPadding; public readonly bool? enableInteractiveSelection; public readonly DragStartBehavior dragStartBehavior; public bool selectionEnabled { get { return this.enableInteractiveSelection ?? !this.obscureText; } } public readonly GestureTapCallback onTap; public readonly InputCounterWidgetBuilder buildCounter; public override State createState() { return new _TextFieldState(); } public override void debugFillProperties(DiagnosticPropertiesBuilder properties) { base.debugFillProperties(properties); properties.add( new DiagnosticsProperty("controller", this.controller, defaultValue: null)); properties.add(new DiagnosticsProperty("focusNode", this.focusNode, defaultValue: null)); properties.add(new DiagnosticsProperty("enabled", this.enabled, defaultValue: null)); properties.add(new DiagnosticsProperty("decoration", this.decoration, defaultValue: new InputDecoration())); properties.add(new DiagnosticsProperty("keyboardType", this.keyboardType, defaultValue: TextInputType.text)); properties.add(new DiagnosticsProperty("style", this.style, defaultValue: null)); properties.add(new DiagnosticsProperty("autofocus", this.autofocus, defaultValue: false)); properties.add(new DiagnosticsProperty("obscureText", this.obscureText, defaultValue: false)); properties.add(new DiagnosticsProperty("autocorrect", this.autocorrect, defaultValue: true)); properties.add(new IntProperty("maxLines", this.maxLines, defaultValue: 1)); properties.add(new IntProperty("maxLength", this.maxLength, defaultValue: null)); properties.add(new FlagProperty("maxLengthEnforced", value: this.maxLengthEnforced, defaultValue: true, ifFalse: "maxLength not enforced")); properties.add(new EnumProperty("textInputAction", this.textInputAction, defaultValue: null)); properties.add(new EnumProperty("textCapitalization", this.textCapitalization, defaultValue: TextCapitalization.none)); properties.add(new EnumProperty("textAlign", this.textAlign, defaultValue: TextAlign.left)); properties.add(new EnumProperty("textDirection", this.textDirection, defaultValue: null)); properties.add(new FloatProperty("cursorWidth", this.cursorWidth, defaultValue: 2.0)); properties.add(new DiagnosticsProperty("cursorRadius", this.cursorRadius, defaultValue: null)); properties.add(new DiagnosticsProperty("cursorColor", this.cursorColor, defaultValue: null)); properties.add(new DiagnosticsProperty("keyboardAppearance", this.keyboardAppearance, defaultValue: null)); properties.add(new DiagnosticsProperty("scrollPadding", this.scrollPadding, defaultValue: EdgeInsets.all(20.0f))); properties.add(new FlagProperty("selectionEnabled", value: this.selectionEnabled, defaultValue: true, ifFalse: "selection disabled")); } } class _TextFieldState : AutomaticKeepAliveClientMixin { readonly GlobalKey _editableTextKey = new LabeledGlobalKey(); HashSet _splashes; InteractiveInkFeature _currentSplash; TextEditingController _controller; TextEditingController _effectiveController { get { return this.widget.controller ?? this._controller; } } FocusNode _focusNode; FocusNode _effectiveFocusNode { get { if (this.widget.focusNode != null) { return this.widget.focusNode; } if (this._focusNode != null) { return this._focusNode; } this._focusNode = new FocusNode(); return this._focusNode; } } bool needsCounter { get { return this.widget.maxLength != null && this.widget.decoration != null && this.widget.decoration.counterText == null; } } InputDecoration _getEffectiveDecoration() { MaterialLocalizations localizations = MaterialLocalizations.of(this.context); ThemeData themeData = Theme.of(this.context); InputDecoration effectiveDecoration = (this.widget.decoration ?? new InputDecoration()) .applyDefaults(themeData.inputDecorationTheme) .copyWith( enabled: this.widget.enabled, hintMaxLines: this.widget.decoration?.hintMaxLines ?? this.widget.maxLines ); if (effectiveDecoration.counter != null || effectiveDecoration.counterText != null) { return effectiveDecoration; } Widget counter; int currentLength = this._effectiveController.value.text.Length; if (effectiveDecoration.counter == null && effectiveDecoration.counterText == null && this.widget.buildCounter != null) { bool isFocused = this._effectiveFocusNode.hasFocus; counter = this.widget.buildCounter( this.context, currentLength: currentLength, maxLength: this.widget.maxLength, isFocused: isFocused ); return effectiveDecoration.copyWith(counter: counter); } if (this.widget.maxLength == null) return effectiveDecoration; string counterText = $"{currentLength}"; if (this.widget.maxLength > 0) { counterText += $"/{this.widget.maxLength}"; if (this._effectiveController.value.text.Length > this.widget.maxLength) { return effectiveDecoration.copyWith( errorText: effectiveDecoration.errorText ?? "", counterStyle: effectiveDecoration.errorStyle ?? themeData.textTheme.caption.copyWith(color: themeData.errorColor), counterText: counterText ); } } // Handle length exceeds maxLength return effectiveDecoration.copyWith( counterText: counterText ); } public override void initState() { base.initState(); if (this.widget.controller == null) { this._controller = new TextEditingController(); } } public override void didUpdateWidget(StatefulWidget oldWidget) { base.didUpdateWidget(oldWidget); if (this.widget.controller == null && ((TextField) oldWidget).controller != null) { this._controller = TextEditingController.fromValue(((TextField) oldWidget).controller.value); } else if (this.widget.controller != null && ((TextField) oldWidget).controller == null) { this._controller = null; } bool isEnabled = this.widget.enabled ?? this.widget.decoration?.enabled ?? true; bool wasEnabled = ((TextField) oldWidget).enabled ?? ((TextField) oldWidget).decoration?.enabled ?? true; if (wasEnabled && !isEnabled) { this._effectiveFocusNode.unfocus(); } } public override void dispose() { this._focusNode?.dispose(); base.dispose(); } void _requestKeyboard() { this._editableTextKey.currentState?.requestKeyboard(); } void _handleSelectionChanged(TextSelection selection, SelectionChangedCause cause) { if (Theme.of(this.context).platform == RuntimePlatform.IPhonePlayer && cause == SelectionChangedCause.longPress) { this._editableTextKey.currentState?.bringIntoView(selection.basePos); } } InteractiveInkFeature _createInkFeature(Offset globalPosition) { MaterialInkController inkController = Material.of(this.context); ThemeData themeData = Theme.of(this.context); BuildContext editableContext = this._editableTextKey.currentContext; RenderBox referenceBox = (RenderBox) (InputDecorator.containerOf(editableContext) ?? editableContext.findRenderObject()); Offset position = referenceBox.globalToLocal(globalPosition); Color color = themeData.splashColor; InteractiveInkFeature splash = null; void handleRemoved() { if (this._splashes != null) { D.assert(this._splashes.Contains(splash)); this._splashes.Remove(splash); if (this._currentSplash == splash) { this._currentSplash = null; } this.updateKeepAlive(); } // else we're probably in deactivate() } splash = themeData.splashFactory.create( controller: inkController, referenceBox: referenceBox, position: position, color: color, containedInkWell: true, borderRadius: BorderRadius.zero, onRemoved: handleRemoved ); return splash; } RenderEditable _renderEditable { get { return this._editableTextKey.currentState.renderEditable; } } void _handleTapDown(TapDownDetails details) { this._renderEditable.handleTapDown(details); this._startSplash(details.globalPosition); } void _handleSingleTapUp(TapUpDetails details) { if (this.widget.enableInteractiveSelection == true) { this._renderEditable.handleTap(); } this._requestKeyboard(); this._confirmCurrentSplash(); if (this.widget.onTap != null) { this.widget.onTap(); } } void _handleSingleTapCancel() { this._cancelCurrentSplash(); } void _handleLongPress() { if (this.widget.enableInteractiveSelection == true) { this._renderEditable.handleLongPress(); } this._confirmCurrentSplash(); } void _handleDragSelectionStart(DragStartDetails details) { this._renderEditable.selectPositionAt( from: details.globalPosition, cause: SelectionChangedCause.drag); this._startSplash(details.globalPosition); } void _handleDragSelectionUpdate(DragStartDetails startDetails, DragUpdateDetails updateDetails) { this._renderEditable.selectPositionAt( from: startDetails.globalPosition, to: updateDetails.globalPosition, cause: SelectionChangedCause.drag); } void _startSplash(Offset globalPosition) { if (this._effectiveFocusNode.hasFocus) { return; } InteractiveInkFeature splash = this._createInkFeature(globalPosition); this._splashes = this._splashes ?? new HashSet(); this._splashes.Add(splash); this._currentSplash = splash; this.updateKeepAlive(); } void _confirmCurrentSplash() { this._currentSplash?.confirm(); this._currentSplash = null; } void _cancelCurrentSplash() { this._currentSplash?.cancel(); } protected override bool wantKeepAlive { get { return this._splashes != null && this._splashes.isNotEmpty(); } } public override void deactivate() { if (this._splashes != null) { HashSet splashes = this._splashes; this._splashes = null; foreach (InteractiveInkFeature splash in splashes) { splash.dispose(); } this._currentSplash = null; } D.assert(this._currentSplash == null); base.deactivate(); } public override Widget build(BuildContext context) { base.build(context); // See AutomaticKeepAliveClientMixin. D.assert(MaterialD.debugCheckHasMaterial(context)); D.assert(WidgetsD.debugCheckHasDirectionality(context)); D.assert( !(this.widget.style != null && this.widget.style.inherit == false && (this.widget.style.fontSize == null || this.widget.style.textBaseline == null)), "inherit false style must supply fontSize and textBaseline" ); ThemeData themeData = Theme.of(context); TextStyle style = themeData.textTheme.subhead.merge(this.widget.style); Brightness keyboardAppearance = this.widget.keyboardAppearance ?? themeData.primaryColorBrightness; TextEditingController controller = this._effectiveController; FocusNode focusNode = this._effectiveFocusNode; List formatters = this.widget.inputFormatters ?? new List(); if (this.widget.maxLength != null && this.widget.maxLengthEnforced) { formatters.Add(new LengthLimitingTextInputFormatter(this.widget.maxLength)); } bool forcePressEnabled = false; TextSelectionControls textSelectionControls = MaterialUtils.materialTextSelectionControls;; bool paintCursorAboveText = false; bool cursorOpacityAnimates = false; Offset cursorOffset = null; Color cursorColor = this.widget.cursorColor ?? themeData.cursorColor; Radius cursorRadius = this.widget.cursorRadius; Widget child = new RepaintBoundary( child: new EditableText( key: this._editableTextKey, controller: controller, focusNode: focusNode, keyboardType: this.widget.keyboardType, textInputAction: this.widget.textInputAction, textCapitalization: this.widget.textCapitalization, style: style, textAlign: this.widget.textAlign, textDirection: this.widget.textDirection, autofocus: this.widget.autofocus, obscureText: this.widget.obscureText, autocorrect: this.widget.autocorrect, maxLines: this.widget.maxLines, selectionColor: themeData.textSelectionColor, selectionControls: this.widget.selectionEnabled ? textSelectionControls : null, onChanged: this.widget.onChanged, onSelectionChanged: this._handleSelectionChanged, onEditingComplete: this.widget.onEditingComplete, onSubmitted: this.widget.onSubmitted, inputFormatters: formatters, rendererIgnoresPointer: true, cursorWidth: this.widget.cursorWidth, cursorRadius: cursorRadius, cursorColor: cursorColor, cursorOpacityAnimates: cursorOpacityAnimates, cursorOffset: cursorOffset, paintCursorAboveText: paintCursorAboveText, backgroundCursorColor: new Color(0xFF8E8E93),// TODO: CupertinoColors.inactiveGray, scrollPadding: this.widget.scrollPadding, keyboardAppearance: keyboardAppearance, enableInteractiveSelection: this.widget.enableInteractiveSelection == true, dragStartBehavior: this.widget.dragStartBehavior ) ); if (this.widget.decoration != null) { child = new AnimatedBuilder( animation: ListenableUtils.merge(new List {focusNode, controller}), builder: (_context, _child) => { return new InputDecorator( decoration: this._getEffectiveDecoration(), baseStyle: this.widget.style, textAlign: this.widget.textAlign, isFocused: focusNode.hasFocus, isEmpty: controller.value.text.isEmpty(), child: _child ); }, child: child ); } return new IgnorePointer( ignoring: !(this.widget.enabled ?? this.widget.decoration?.enabled ?? true), child: new TextSelectionGestureDetector( onTapDown: this._handleTapDown, // onForcePressStart: forcePressEnabled ? this._handleForcePressStarted : null, // TODO: Remove this when force press is added onSingleTapUp: this._handleSingleTapUp, onSingleTapCancel: this._handleSingleTapCancel, onSingleLongTapStart: this._handleLongPress, onDragSelectionStart: this._handleDragSelectionStart, onDragSelectionUpdate: this._handleDragSelectionUpdate, behavior: HitTestBehavior.translucent, child: child ) ); } } }