using System; using System.Collections.Generic; using Unity.UIWidgets.foundation; using Unity.UIWidgets.gestures; using Unity.UIWidgets.painting; using Unity.UIWidgets.service; using Unity.UIWidgets.ui; using UnityEngine; using Canvas = Unity.UIWidgets.ui.Canvas; using Color = Unity.UIWidgets.ui.Color; using Rect = Unity.UIWidgets.ui.Rect; namespace Unity.UIWidgets.rendering { public enum TextOverflow { /// Clip the overflowing text to fix its container. clip, /// Fade the overflowing text to transparent. fade, /// Use an ellipsis to indicate that the text has overflowed. ellipsis, /// Render overflowing text outside of its container. visible, } public class RenderParagraph : RenderBox { static readonly string _kEllipsis = "\u2026"; bool _softWrap; TextOverflow _overflow; readonly TextPainter _textPainter; bool _needsClipping = false; List _selectionRects; public RenderParagraph(TextSpan text, TextAlign textAlign = TextAlign.left, TextDirection textDirection = TextDirection.ltr, bool softWrap = true, TextOverflow overflow = TextOverflow.clip, float textScaleFactor = 1.0f, int? maxLines = null, StrutStyle strutStyle = null, Action onSelectionChanged = null, Color selectionColor = null ) { D.assert(maxLines == null || maxLines > 0); _softWrap = softWrap; _overflow = overflow; _textPainter = new TextPainter( text, textAlign, textDirection, textScaleFactor, maxLines, overflow == TextOverflow.ellipsis ? _kEllipsis : "", strutStyle: strutStyle ); _selection = null; this.onSelectionChanged = onSelectionChanged; this.selectionColor = selectionColor; _resetHoverHandler(); } public Action onSelectionChanged; public Color selectionColor; public TextSelection selection { get { return _selection; } set { if (_selection == value) { return; } _selection = value; _selectionRects = null; markNeedsPaint(); } } public TextSpan text { get { return _textPainter.text; } set { Debug.Assert(value != null); switch (_textPainter.text.compareTo(value)) { case RenderComparison.identical: case RenderComparison.metadata: return; case RenderComparison.function: _textPainter.text = value; markNeedsPaint(); break; case RenderComparison.paint: _textPainter.text = value; markNeedsPaint(); break; case RenderComparison.layout: _textPainter.text = value; markNeedsLayout(); break; } _resetHoverHandler(); } } public TextAlign textAlign { get { return _textPainter.textAlign; } set { if (_textPainter.textAlign == value) { return; } _textPainter.textAlign = value; markNeedsPaint(); } } public TextDirection? textDirection { get { return _textPainter.textDirection; } set { if (_textPainter.textDirection == value) { return; } _textPainter.textDirection = textDirection; markNeedsLayout(); } } protected Offset getOffsetForCaret(TextPosition position, Rect caretPrototype) { D.assert(_textPainter != null); return _textPainter.getOffsetForCaret(position, caretPrototype); } public bool softWrap { get { return _softWrap; } set { if (_softWrap == value) { return; } _softWrap = value; markNeedsLayout(); } } public TextOverflow overflow { get { return _overflow; } set { if (_overflow == value) { return; } _overflow = value; _textPainter.ellipsis = value == TextOverflow.ellipsis ? _kEllipsis : null; // _textPainter.e markNeedsLayout(); } } public float textScaleFactor { get { return _textPainter.textScaleFactor; } set { if (Mathf.Abs(_textPainter.textScaleFactor - value) < 0.00000001) { return; } _textPainter.textScaleFactor = value; markNeedsLayout(); } } public int? maxLines { get { return _textPainter.maxLines; } set { D.assert(maxLines == null || maxLines > 0); if (_textPainter.maxLines == value) { return; } _textPainter.maxLines = value; markNeedsLayout(); } } public Size textSize { get { return _textPainter.size; } } protected override float computeMinIntrinsicWidth(float height) { _layoutText(); return _textPainter.minIntrinsicWidth; } protected override float computeMaxIntrinsicWidth(float height) { _layoutText(); return _textPainter.maxIntrinsicWidth; } float _computeIntrinsicHeight(float width) { _layoutText(minWidth: width, maxWidth: width); return _textPainter.height; } protected override float computeMinIntrinsicHeight(float width) { return _computeIntrinsicHeight(width); } protected internal override float computeMaxIntrinsicHeight(float width) { return _computeIntrinsicHeight(width); } protected override float? computeDistanceToActualBaseline(TextBaseline baseline) { _layoutTextWithConstraints(constraints); return _textPainter.computeDistanceToActualBaseline(baseline); } protected override bool hitTestSelf(Offset position) { return true; } bool _hasFocus = false; bool _listenerAttached = false; public bool hasFocus { get { return _hasFocus; } set { if (_hasFocus == value) { return; } _hasFocus = value; if (_hasFocus) { D.assert(!_listenerAttached); RawKeyboard.instance.addListener(_handleKeyEvent); _listenerAttached = true; } else { selection = null; D.assert(_listenerAttached); RawKeyboard.instance.removeListener(_handleKeyEvent); _listenerAttached = false; } } } TextSpan _previousHoverSpan; #pragma warning disable 0414 bool _pointerHoverInside; #pragma warning restore 0414 bool _hasHoverRecognizer; MouseTrackerAnnotation _hoverAnnotation; void _resetHoverHandler() { _hasHoverRecognizer = _textPainter.text.hasHoverRecognizer; _previousHoverSpan = null; _pointerHoverInside = false; if (_hoverAnnotation != null && attached) { RendererBinding.instance.mouseTracker.detachAnnotation(_hoverAnnotation); } if (_hasHoverRecognizer) { _hoverAnnotation = new MouseTrackerAnnotation( onEnter: _onPointerEnter, onHover: _onPointerHover, onExit: _onPointerExit); if (attached) { RendererBinding.instance.mouseTracker.attachAnnotation(_hoverAnnotation); } } else { _hoverAnnotation = null; } } void _handleKeyEvent(RawKeyEvent keyEvent) { //only allow KCommand.copy if (keyEvent is RawKeyUpEvent) { return; } if (selection.isCollapsed) { return; } KeyCode pressedKeyCode = keyEvent.data.unityEvent.keyCode; int modifiers = (int) keyEvent.data.unityEvent.modifiers; bool ctrl = (modifiers & (int) EventModifiers.Control) > 0; bool cmd = (modifiers & (int) EventModifiers.Command) > 0; bool cKey = pressedKeyCode == KeyCode.C; bool isMac = SystemInfo.operatingSystemFamily == OperatingSystemFamily.MacOSX; KeyCommand? kcmd = keyEvent is RawKeyCommandEvent ? ((RawKeyCommandEvent) keyEvent).command : ((ctrl || (isMac && cmd)) && cKey) ? KeyCommand.Copy : (KeyCommand?) null; if (kcmd == KeyCommand.Copy) { Clipboard.setData( new ClipboardData(text: selection.textInside(text.toPlainText())) ); } } public override void attach(object owner) { base.attach(owner); if (_hoverAnnotation != null) { RendererBinding.instance.mouseTracker.attachAnnotation(_hoverAnnotation); } } public override void detach() { if (_listenerAttached) { RawKeyboard.instance.removeListener(_handleKeyEvent); } base.detach(); if (_hoverAnnotation != null) { RendererBinding.instance.mouseTracker.detachAnnotation(_hoverAnnotation); } } TextSelection _selection; public void selectPositionAt(Offset from = null, Offset to = null, SelectionChangedCause? cause = null) { D.assert(cause != null); D.assert(from != null); if (true) { TextPosition fromPosition = _textPainter.getPositionForOffset(globalToLocal(from)); TextPosition toPosition = to == null ? null : _textPainter.getPositionForOffset(globalToLocal(to)); int baseOffset = fromPosition.offset; int extentOffset = fromPosition.offset; if (toPosition != null) { baseOffset = Mathf.Min(fromPosition.offset, toPosition.offset); extentOffset = Mathf.Max(fromPosition.offset, toPosition.offset); } TextSelection newSelection = new TextSelection( baseOffset: baseOffset, extentOffset: extentOffset, affinity: fromPosition.affinity); if (newSelection != _selection) { _handleSelectionChanged(newSelection, cause.Value); } } } void _handleSelectionChanged(TextSelection selection, SelectionChangedCause cause) { this.selection = selection; onSelectionChanged?.Invoke(); } void _onPointerEnter(PointerEvent evt) { _pointerHoverInside = true; } void _onPointerExit(PointerEvent evt) { _pointerHoverInside = false; _previousHoverSpan?.hoverRecognizer?.OnPointerLeave?.Invoke(); _previousHoverSpan = null; } void _onPointerHover(PointerEvent evt) { _layoutTextWithConstraints(constraints); Offset offset = globalToLocal(evt.position); TextPosition position = _textPainter.getPositionForOffset(offset); TextSpan span = _textPainter.text.getSpanForPosition(position); if (_previousHoverSpan != span) { _previousHoverSpan?.hoverRecognizer?.OnPointerLeave?.Invoke(); span?.hoverRecognizer?.OnPointerEnter?.Invoke((PointerHoverEvent) evt); _previousHoverSpan = span; } } public override void handleEvent(PointerEvent evt, HitTestEntry entry) { D.assert(debugHandleEvent(evt, entry)); if (!(evt is PointerDownEvent)) { return; } _layoutTextWithConstraints(constraints); Offset offset = ((BoxHitTestEntry) entry).localPosition; TextPosition position = _textPainter.getPositionForOffset(offset); TextSpan span = _textPainter.text.getSpanForPosition(position); span?.recognizer?.addPointer((PointerDownEvent) evt); } protected override void performLayout() { _layoutTextWithConstraints(constraints); var textSize = _textPainter.size; var textDidExceedMaxLines = _textPainter.didExceedMaxLines; size = constraints.constrain(textSize); var didOverflowHeight = size.height < textSize.height || textDidExceedMaxLines; var didOverflowWidth = size.width < textSize.width; var hasVisualOverflow = didOverflowWidth || didOverflowHeight; if (hasVisualOverflow) { switch (_overflow) { case TextOverflow.visible: _needsClipping = false; break; case TextOverflow.clip: case TextOverflow.ellipsis: case TextOverflow.fade: _needsClipping = true; break; } } else { _needsClipping = false; } _selectionRects = null; } void paintParagraph(PaintingContext context, Offset offset) { _layoutTextWithConstraints(constraints); var canvas = context.canvas; if (_needsClipping) { var bounds = offset & size; canvas.save(); canvas.clipRect(bounds); } if (_selection != null && selectionColor != null && _selection.isValid) { if (!_selection.isCollapsed) { _selectionRects = _selectionRects ?? _textPainter.getBoxesForSelection(_selection); _paintSelection(canvas, offset); } } _textPainter.paint(canvas, offset); if (_needsClipping) { canvas.restore(); } } public override void paint(PaintingContext context, Offset offset) { if (_hoverAnnotation != null) { AnnotatedRegionLayer layer = new AnnotatedRegionLayer( _hoverAnnotation, size: size, offset: offset); context.pushLayer(layer, paintParagraph, offset); } else { paintParagraph(context, offset); } } void _paintSelection(Canvas canvas, Offset effectiveOffset) { D.assert(_selectionRects != null); D.assert(selectionColor != null); var paint = new Paint {color = selectionColor}; Path barPath = new Path(); foreach (var box in _selectionRects) { barPath.addRect(box.toRect().shift(effectiveOffset)); } canvas.drawPath(barPath, paint); } public StrutStyle strutStyle { get { return _textPainter.strutStyle; } set { if (_textPainter.strutStyle == value) { return; } _textPainter.strutStyle = value; markNeedsLayout(); } } void _layoutText(float minWidth = 0.0f, float maxWidth = float.PositiveInfinity) { var widthMatters = softWrap || overflow == TextOverflow.ellipsis; _textPainter.layout(minWidth, widthMatters ? maxWidth : float.PositiveInfinity); } void _layoutTextWithConstraints(BoxConstraints constraints) { _layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth); } public override List debugDescribeChildren() { return new List { text.toDiagnosticsNode(name: "text", style: DiagnosticsTreeStyle.transition) }; } public override void debugFillProperties(DiagnosticPropertiesBuilder properties) { base.debugFillProperties(properties); properties.add(new EnumProperty("textAlign", textAlign)); properties.add(new EnumProperty("textDirection", textDirection)); properties.add(new FlagProperty("softWrap", value: softWrap, ifTrue: "wrapping at box width", ifFalse: "no wrapping except at line break characters", showName: true)); properties.add(new EnumProperty("overflow", overflow)); properties.add(new FloatProperty("textScaleFactor", textScaleFactor, defaultValue: 1.0f)); properties.add(new IntProperty("maxLines", maxLines, ifNull: "unlimited")); } } }