iOS - floating cursor

pull/519/head
X Code 3 years ago
parent 2b8fc44a5d
commit 13d542680e
  1. 4
      lib/src/widgets/box.dart
  2. 11
      lib/src/widgets/cursor.dart
  3. 196
      lib/src/widgets/editor.dart
  4. 31
      lib/src/widgets/float_cursor.dart
  5. 43
      lib/src/widgets/raw_editor.dart
  6. 115
      lib/src/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart
  7. 48
      lib/src/widgets/simple_viewer.dart
  8. 10
      lib/src/widgets/text_block.dart
  9. 55
      lib/src/widgets/text_line.dart

@ -119,4 +119,8 @@ abstract class RenderEditableBox extends RenderBox {
/// Returns the [Rect] in local coordinates for the caret at the given text /// Returns the [Rect] in local coordinates for the caret at the given text
/// position. /// position.
Rect getLocalRectForCaret(TextPosition position); Rect getLocalRectForCaret(TextPosition position);
/// Returns the [Rect] of the caret prototype at the given text
/// position. [Rect] starts at origin.
Rect getCaretPrototype(TextPosition position);
} }

@ -131,6 +131,17 @@ class CursorCont extends ChangeNotifier {
Timer? _cursorTimer; Timer? _cursorTimer;
bool _targetCursorVisibility = false; bool _targetCursorVisibility = false;
final ValueNotifier<TextPosition?> _floatingCursorTextPosition =
ValueNotifier(null);
ValueNotifier<TextPosition?> get floatingCursorTextPosition =>
_floatingCursorTextPosition;
void setFloatingCursorTextPosition(TextPosition? position) =>
_floatingCursorTextPosition.value = position;
bool get isFloatingCursorActive => floatingCursorTextPosition.value != null;
CursorStyle _style; CursorStyle _style;
CursorStyle get style => _style; CursorStyle get style => _style;
set style(CursorStyle value) { set style(CursorStyle value) {

@ -7,6 +7,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:string_validator/string_validator.dart'; import 'package:string_validator/string_validator.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
@ -22,6 +23,7 @@ import 'controller.dart';
import 'cursor.dart'; import 'cursor.dart';
import 'default_styles.dart'; import 'default_styles.dart';
import 'delegate.dart'; import 'delegate.dart';
import 'float_cursor.dart';
import 'image.dart'; import 'image.dart';
import 'raw_editor.dart'; import 'raw_editor.dart';
import 'text_selection.dart'; import 'text_selection.dart';
@ -56,6 +58,10 @@ abstract class EditorState extends State<RawEditor> {
EditorTextSelectionOverlay? getSelectionOverlay(); EditorTextSelectionOverlay? getSelectionOverlay();
/// Controls the floating cursor animation when it is released.
/// The floating cursor is animated to merge with the regular cursor.
AnimationController get floatingCursorResetController;
bool showToolbar(); bool showToolbar();
void hideToolbar(); void hideToolbar();
@ -88,9 +94,24 @@ abstract class RenderAbstractEditor {
/// selection that contains some text but whose ends meet in the middle). /// selection that contains some text but whose ends meet in the middle).
TextPosition getPositionForOffset(Offset offset); TextPosition getPositionForOffset(Offset offset);
/// Returns the local coordinates of the endpoints of the given selection.
///
/// If the selection is collapsed (and therefore occupies a single point), the
/// returned list is of length one. Otherwise, the selection is not collapsed
/// and the returned list is of length two. In this case, however, the two
/// points might actually be co-located (e.g., because of a bidirectional
/// selection that contains some text but whose ends meet in the middle).
List<TextSelectionPoint> getEndpointsForSelection( List<TextSelectionPoint> getEndpointsForSelection(
TextSelection textSelection); TextSelection textSelection);
/// Sets the screen position of the floating cursor and the text position
/// closest to the cursor.
/// `resetLerpValue` drives the size of the floating cursor.
/// See [EditorState.floatingCursorResetController].
void setFloatingCursor(FloatingCursorDragState dragState,
Offset lastBoundedOffset, TextPosition lastTextPosition,
{double? resetLerpValue});
/// If [ignorePointer] is false (the default) then this method is called by /// If [ignorePointer] is false (the default) then this method is called by
/// the internal gesture recognizer's [TapGestureRecognizer.onTapDown] /// the internal gesture recognizer's [TapGestureRecognizer.onTapDown]
/// callback. /// callback.
@ -646,25 +667,39 @@ class _QuillEditorSelectionGestureDetectorBuilder
} }
} }
/// Signature for the callback that reports when the user changes the selection
/// (including the cursor location).
///
/// Used by [RenderEditor.onSelectionChanged].
typedef TextSelectionChangedHandler = void Function( typedef TextSelectionChangedHandler = void Function(
TextSelection selection, SelectionChangedCause cause); TextSelection selection, SelectionChangedCause cause);
// The padding applied to text field. Used to determine the bounds when
// moving the floating cursor.
const EdgeInsets _kFloatingCursorAddedMargin = EdgeInsets.fromLTRB(4, 4, 4, 5);
// The additional size on the x and y axis with which to expand the prototype
// cursor to render the floating cursor in pixels.
const EdgeInsets _kFloatingCaretSizeIncrease =
EdgeInsets.symmetric(horizontal: 0.5, vertical: 1);
class RenderEditor extends RenderEditableContainerBox class RenderEditor extends RenderEditableContainerBox
implements RenderAbstractEditor { implements RenderAbstractEditor {
RenderEditor( RenderEditor(
ViewportOffset? offset, ViewportOffset? offset,
List<RenderEditableBox>? children, List<RenderEditableBox>? children,
TextDirection textDirection, TextDirection textDirection,
double scrollBottomInset, double scrollBottomInset,
EdgeInsetsGeometry padding, EdgeInsetsGeometry padding,
this.document, this.document,
this.selection, this.selection,
this._hasFocus, this._hasFocus,
this.onSelectionChanged, this.onSelectionChanged,
this._startHandleLayerLink, this._startHandleLayerLink,
this._endHandleLayerLink, this._endHandleLayerLink,
EdgeInsets floatingCursorAddedMargin, EdgeInsets floatingCursorAddedMargin,
) : super( this._cursorController)
: super(
children, children,
document.root, document.root,
textDirection, textDirection,
@ -672,6 +707,8 @@ class RenderEditor extends RenderEditableContainerBox
padding, padding,
); );
final CursorCont _cursorController;
Document document; Document document;
TextSelection selection; TextSelection selection;
bool _hasFocus = false; bool _hasFocus = false;
@ -983,9 +1020,20 @@ class RenderEditor extends RenderEditableContainerBox
@override @override
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
if (_hasFocus &&
_cursorController.show.value &&
!_cursorController.style.paintAboveText) {
_paintFloatingCursor(context, offset);
}
defaultPaint(context, offset); defaultPaint(context, offset);
_updateSelectionExtentsVisibility(offset + _paintOffset); _updateSelectionExtentsVisibility(offset + _paintOffset);
_paintHandleLayers(context, getEndpointsForSelection(selection)); _paintHandleLayers(context, getEndpointsForSelection(selection));
if (_hasFocus &&
_cursorController.show.value &&
_cursorController.style.paintAboveText) {
_paintFloatingCursor(context, offset);
}
} }
@override @override
@ -1097,6 +1145,128 @@ class RenderEditor extends RenderEditableContainerBox
final boxParentData = targetChild.parentData as BoxParentData; final boxParentData = targetChild.parentData as BoxParentData;
return childLocalRect.shift(Offset(0, boxParentData.offset.dy)); return childLocalRect.shift(Offset(0, boxParentData.offset.dy));
} }
// Start floating cursor
FloatingCursorPainter get _floatingCursorPainter => FloatingCursorPainter(
floatingCursorRect: _floatingCursorRect,
style: _cursorController.style,
);
bool _floatingCursorOn = false;
Rect? _floatingCursorRect;
TextPosition get floatingCursorTextPosition => _floatingCursorTextPosition;
late TextPosition _floatingCursorTextPosition;
// The relative origin in relation to the distance the user has theoretically
// dragged the floating cursor offscreen.
// This value is used to account for the difference
// in the rendering position and the raw offset value.
Offset _relativeOrigin = Offset.zero;
Offset? _previousOffset;
bool _resetOriginOnLeft = false;
bool _resetOriginOnRight = false;
bool _resetOriginOnTop = false;
bool _resetOriginOnBottom = false;
/// Returns the position within the editor closest to the raw cursor offset.
Offset calculateBoundedFloatingCursorOffset(
Offset rawCursorOffset, double preferredLineHeight) {
var deltaPosition = Offset.zero;
final topBound = _kFloatingCursorAddedMargin.top;
final bottomBound =
size.height - preferredLineHeight + _kFloatingCursorAddedMargin.bottom;
final leftBound = _kFloatingCursorAddedMargin.left;
final rightBound = size.width - _kFloatingCursorAddedMargin.right;
if (_previousOffset != null) {
deltaPosition = rawCursorOffset - _previousOffset!;
}
// If the raw cursor offset has gone off an edge,
// we want to reset the relative origin of
// the dragging when the user drags back into the field.
if (_resetOriginOnLeft && deltaPosition.dx > 0) {
_relativeOrigin =
Offset(rawCursorOffset.dx - leftBound, _relativeOrigin.dy);
_resetOriginOnLeft = false;
} else if (_resetOriginOnRight && deltaPosition.dx < 0) {
_relativeOrigin =
Offset(rawCursorOffset.dx - rightBound, _relativeOrigin.dy);
_resetOriginOnRight = false;
}
if (_resetOriginOnTop && deltaPosition.dy > 0) {
_relativeOrigin =
Offset(_relativeOrigin.dx, rawCursorOffset.dy - topBound);
_resetOriginOnTop = false;
} else if (_resetOriginOnBottom && deltaPosition.dy < 0) {
_relativeOrigin =
Offset(_relativeOrigin.dx, rawCursorOffset.dy - bottomBound);
_resetOriginOnBottom = false;
}
final currentX = rawCursorOffset.dx - _relativeOrigin.dx;
final currentY = rawCursorOffset.dy - _relativeOrigin.dy;
final double adjustedX =
math.min(math.max(currentX, leftBound), rightBound);
final double adjustedY =
math.min(math.max(currentY, topBound), bottomBound);
final adjustedOffset = Offset(adjustedX, adjustedY);
if (currentX < leftBound && deltaPosition.dx < 0) {
_resetOriginOnLeft = true;
} else if (currentX > rightBound && deltaPosition.dx > 0) {
_resetOriginOnRight = true;
}
if (currentY < topBound && deltaPosition.dy < 0) {
_resetOriginOnTop = true;
} else if (currentY > bottomBound && deltaPosition.dy > 0) {
_resetOriginOnBottom = true;
}
_previousOffset = rawCursorOffset;
return adjustedOffset;
}
@override
void setFloatingCursor(FloatingCursorDragState dragState,
Offset boundedOffset, TextPosition textPosition,
{double? resetLerpValue}) {
if (dragState == FloatingCursorDragState.Start) {
_relativeOrigin = Offset.zero;
_previousOffset = null;
_resetOriginOnBottom = false;
_resetOriginOnTop = false;
_resetOriginOnRight = false;
_resetOriginOnBottom = false;
}
_floatingCursorOn = dragState != FloatingCursorDragState.End;
if (_floatingCursorOn) {
_floatingCursorTextPosition = textPosition;
final sizeAdjustment = resetLerpValue != null
? EdgeInsets.lerp(
_kFloatingCaretSizeIncrease, EdgeInsets.zero, resetLerpValue)!
: _kFloatingCaretSizeIncrease;
final child = childAtPosition(textPosition);
final caretPrototype =
child.getCaretPrototype(child.globalToLocalPosition(textPosition));
_floatingCursorRect =
sizeAdjustment.inflateRect(caretPrototype).shift(boundedOffset);
_cursorController
.setFloatingCursorTextPosition(_floatingCursorTextPosition);
} else {
_floatingCursorRect = null;
_cursorController.setFloatingCursorTextPosition(null);
}
}
void _paintFloatingCursor(PaintingContext context, Offset offset) {
_floatingCursorPainter.paint(context.canvas);
}
// End floating cursor
} }
class EditableContainerParentData class EditableContainerParentData

@ -0,0 +1,31 @@
// The corner radius of the floating cursor in pixels.
import 'dart:ui';
import '../../widgets/cursor.dart';
const Radius _kFloatingCaretRadius = Radius.circular(1);
/// Floating painter responsible for painting the floating cursor when
/// floating mode is activated
class FloatingCursorPainter {
FloatingCursorPainter({
required this.floatingCursorRect,
required this.style,
});
CursorStyle style;
Rect? floatingCursorRect;
final Paint floatingCursorPaint = Paint();
void paint(Canvas canvas) {
final floatingCursorRect = this.floatingCursorRect;
final floatingCursorColor = style.color.withOpacity(0.75);
if (floatingCursorRect == null) return;
canvas.drawRRect(
RRect.fromRectAndRadius(floatingCursorRect, _kFloatingCaretRadius),
floatingCursorPaint..color = floatingCursorColor,
);
}
}

@ -126,6 +126,7 @@ class RawEditorState extends EditorState
ScrollController get scrollController => _scrollController; ScrollController get scrollController => _scrollController;
late ScrollController _scrollController; late ScrollController _scrollController;
// Cursors
late CursorCont _cursorCont; late CursorCont _cursorCont;
// Focus // Focus
@ -133,6 +134,7 @@ class RawEditorState extends EditorState
FocusAttachment? _focusAttachment; FocusAttachment? _focusAttachment;
bool get _hasFocus => widget.focusNode.hasFocus; bool get _hasFocus => widget.focusNode.hasFocus;
// Theme
DefaultStyles? _styles; DefaultStyles? _styles;
final ClipboardStatusNotifier _clipboardStatus = ClipboardStatusNotifier(); final ClipboardStatusNotifier _clipboardStatus = ClipboardStatusNotifier();
@ -162,6 +164,7 @@ class RawEditorState extends EditorState
document: _doc, document: _doc,
selection: widget.controller.selection, selection: widget.controller.selection,
hasFocus: _hasFocus, hasFocus: _hasFocus,
cursorController: _cursorCont,
textDirection: _textDirection, textDirection: _textDirection,
startHandleLayerLink: _startHandleLayerLink, startHandleLayerLink: _startHandleLayerLink,
endHandleLayerLink: _endHandleLayerLink, endHandleLayerLink: _endHandleLayerLink,
@ -196,6 +199,7 @@ class RawEditorState extends EditorState
onSelectionChanged: _handleSelectionChanged, onSelectionChanged: _handleSelectionChanged,
scrollBottomInset: widget.scrollBottomInset, scrollBottomInset: widget.scrollBottomInset,
padding: widget.padding, padding: widget.padding,
cursorController: _cursorCont,
children: _buildChildren(_doc, context), children: _buildChildren(_doc, context),
), ),
), ),
@ -362,6 +366,11 @@ class RawEditorState extends EditorState
tickerProvider: this, tickerProvider: this,
); );
// Floating cursor
_floatingCursorResetController = AnimationController(vsync: this);
_floatingCursorResetController.addListener(onFloatingCursorResetTick);
// Keyboard
_keyboardListener = KeyboardEventHandler( _keyboardListener = KeyboardEventHandler(
handleCursorMovement, handleCursorMovement,
handleShortcut, handleShortcut,
@ -727,6 +736,12 @@ class RawEditorState extends EditorState
@override @override
bool get readOnly => widget.readOnly; bool get readOnly => widget.readOnly;
@override
AnimationController get floatingCursorResetController =>
_floatingCursorResetController;
late AnimationController _floatingCursorResetController;
} }
class _Editor extends MultiChildRenderObjectWidget { class _Editor extends MultiChildRenderObjectWidget {
@ -741,6 +756,7 @@ class _Editor extends MultiChildRenderObjectWidget {
required this.endHandleLayerLink, required this.endHandleLayerLink,
required this.onSelectionChanged, required this.onSelectionChanged,
required this.scrollBottomInset, required this.scrollBottomInset,
required this.cursorController,
this.padding = EdgeInsets.zero, this.padding = EdgeInsets.zero,
this.offset, this.offset,
}) : super(key: key, children: children); }) : super(key: key, children: children);
@ -755,23 +771,24 @@ class _Editor extends MultiChildRenderObjectWidget {
final TextSelectionChangedHandler onSelectionChanged; final TextSelectionChangedHandler onSelectionChanged;
final double scrollBottomInset; final double scrollBottomInset;
final EdgeInsetsGeometry padding; final EdgeInsetsGeometry padding;
final CursorCont cursorController;
@override @override
RenderEditor createRenderObject(BuildContext context) { RenderEditor createRenderObject(BuildContext context) {
return RenderEditor( return RenderEditor(
offset, offset,
null, null,
textDirection, textDirection,
scrollBottomInset, scrollBottomInset,
padding, padding,
document, document,
selection, selection,
hasFocus, hasFocus,
onSelectionChanged, onSelectionChanged,
startHandleLayerLink, startHandleLayerLink,
endHandleLayerLink, endHandleLayerLink,
const EdgeInsets.fromLTRB(4, 4, 4, 5), const EdgeInsets.fromLTRB(4, 4, 4, 5),
); cursorController);
} }
@override @override

@ -1,3 +1,6 @@
import 'dart:ui';
import 'package:flutter/animation.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -185,9 +188,119 @@ mixin RawEditorStateTextInputClientMixin on EditorState
// no-op // no-op
} }
// The time it takes for the floating cursor to snap to the text aligned
// cursor position after the user has finished placing it.
static const Duration _floatingCursorResetTime = Duration(milliseconds: 125);
// The original position of the caret on FloatingCursorDragState.start.
Rect? _startCaretRect;
// The most recent text position as determined by the location of the floating
// cursor.
TextPosition? _lastTextPosition;
// The offset of the floating cursor as determined from the start call.
Offset? _pointOffsetOrigin;
// The most recent position of the floating cursor.
Offset? _lastBoundedOffset;
// Because the center of the cursor is preferredLineHeight / 2 below the touch
// origin, but the touch origin is used to determine which line the cursor is
// on, we need this offset to correctly render and move the cursor.
Offset _floatingCursorOffset(TextPosition textPosition) =>
Offset(0, getRenderEditor()!.preferredLineHeight(textPosition) / 2);
@override @override
void updateFloatingCursor(RawFloatingCursorPoint point) { void updateFloatingCursor(RawFloatingCursorPoint point) {
throw UnimplementedError(); switch (point.state) {
case FloatingCursorDragState.Start:
if (floatingCursorResetController.isAnimating) {
floatingCursorResetController.stop();
onFloatingCursorResetTick();
}
// We want to send in points that are centered around a (0,0) origin, so
// we cache the position.
_pointOffsetOrigin = point.offset;
final currentTextPosition =
TextPosition(offset: getRenderEditor()!.selection.baseOffset);
_startCaretRect =
getRenderEditor()!.getLocalRectForCaret(currentTextPosition);
_lastBoundedOffset = _startCaretRect!.center -
_floatingCursorOffset(currentTextPosition);
_lastTextPosition = currentTextPosition;
getRenderEditor()!.setFloatingCursor(
point.state, _lastBoundedOffset!, _lastTextPosition!);
break;
case FloatingCursorDragState.Update:
assert(_lastTextPosition != null, 'Last text position was not set');
final floatingCursorOffset = _floatingCursorOffset(_lastTextPosition!);
final centeredPoint = point.offset! - _pointOffsetOrigin!;
final rawCursorOffset =
_startCaretRect!.center + centeredPoint - floatingCursorOffset;
final preferredLineHeight =
getRenderEditor()!.preferredLineHeight(_lastTextPosition!);
_lastBoundedOffset =
getRenderEditor()!.calculateBoundedFloatingCursorOffset(
rawCursorOffset,
preferredLineHeight,
);
_lastTextPosition = getRenderEditor()!.getPositionForOffset(
getRenderEditor()!
.localToGlobal(_lastBoundedOffset! + floatingCursorOffset));
getRenderEditor()!.setFloatingCursor(
point.state, _lastBoundedOffset!, _lastTextPosition!);
final newSelection = TextSelection.collapsed(
offset: _lastTextPosition!.offset,
affinity: _lastTextPosition!.affinity);
// Setting selection as floating cursor moves will have scroll view
// bring background cursor into view
getRenderEditor()!
.onSelectionChanged(newSelection, SelectionChangedCause.forcePress);
break;
case FloatingCursorDragState.End:
// We skip animation if no update has happened.
if (_lastTextPosition != null && _lastBoundedOffset != null) {
floatingCursorResetController
..value = 0.0
..animateTo(1,
duration: _floatingCursorResetTime, curve: Curves.decelerate);
}
break;
}
}
/// Specifies the floating cursor dimensions and position based
/// the animation controller value.
/// The floating cursor is resized
/// (see [RenderAbstractEditor.setFloatingCursor])
/// and repositioned (linear interpolation between position of floating cursor
/// and current position of background cursor)
void onFloatingCursorResetTick() {
final finalPosition =
getRenderEditor()!.getLocalRectForCaret(_lastTextPosition!).centerLeft -
_floatingCursorOffset(_lastTextPosition!);
if (floatingCursorResetController.isCompleted) {
getRenderEditor()!.setFloatingCursor(
FloatingCursorDragState.End, finalPosition, _lastTextPosition!);
_startCaretRect = null;
_lastTextPosition = null;
_pointOffsetOrigin = null;
_lastBoundedOffset = null;
} else {
final lerpValue = floatingCursorResetController.value;
final lerpX =
lerpDouble(_lastBoundedOffset!.dx, finalPosition.dx, lerpValue)!;
final lerpY =
lerpDouble(_lastBoundedOffset!.dy, finalPosition.dy, lerpValue)!;
getRenderEditor()!.setFloatingCursor(FloatingCursorDragState.Update,
Offset(lerpX, lerpY), _lastTextPosition!,
resetLerpValue: lerpValue);
}
} }
@override @override

@ -150,15 +150,15 @@ class _QuillSimpleViewerState extends State<QuillSimpleViewer>
link: _toolbarLayerLink, link: _toolbarLayerLink,
child: Semantics( child: Semantics(
child: _SimpleViewer( child: _SimpleViewer(
document: _doc, document: _doc,
textDirection: _textDirection, textDirection: _textDirection,
startHandleLayerLink: _startHandleLayerLink, startHandleLayerLink: _startHandleLayerLink,
endHandleLayerLink: _endHandleLayerLink, endHandleLayerLink: _endHandleLayerLink,
onSelectionChanged: _nullSelectionChanged, onSelectionChanged: _nullSelectionChanged,
scrollBottomInset: widget.scrollBottomInset, scrollBottomInset: widget.scrollBottomInset,
padding: widget.padding, padding: widget.padding,
children: _buildChildren(_doc, context), cursorController: _cursorCont,
), children: _buildChildren(_doc, context)),
), ),
); );
@ -315,6 +315,7 @@ class _SimpleViewer extends MultiChildRenderObjectWidget {
required this.endHandleLayerLink, required this.endHandleLayerLink,
required this.onSelectionChanged, required this.onSelectionChanged,
required this.scrollBottomInset, required this.scrollBottomInset,
required this.cursorController,
this.offset, this.offset,
this.padding = EdgeInsets.zero, this.padding = EdgeInsets.zero,
Key? key, Key? key,
@ -328,24 +329,25 @@ class _SimpleViewer extends MultiChildRenderObjectWidget {
final TextSelectionChangedHandler onSelectionChanged; final TextSelectionChangedHandler onSelectionChanged;
final double scrollBottomInset; final double scrollBottomInset;
final EdgeInsetsGeometry padding; final EdgeInsetsGeometry padding;
final CursorCont cursorController;
@override @override
RenderEditor createRenderObject(BuildContext context) { RenderEditor createRenderObject(BuildContext context) {
return RenderEditor( return RenderEditor(
offset, offset,
null, null,
textDirection, textDirection,
scrollBottomInset, scrollBottomInset,
padding, padding,
document, document,
const TextSelection(baseOffset: 0, extentOffset: 0), const TextSelection(baseOffset: 0, extentOffset: 0),
false, false,
// hasFocus, // hasFocus,
onSelectionChanged, onSelectionChanged,
startHandleLayerLink, startHandleLayerLink,
endHandleLayerLink, endHandleLayerLink,
const EdgeInsets.fromLTRB(4, 4, 4, 5), const EdgeInsets.fromLTRB(4, 4, 4, 5),
); cursorController);
} }
@override @override

@ -560,6 +560,16 @@ class RenderEditableTextBlock extends RenderEditableContainerBox
affinity: position.affinity, affinity: position.affinity,
); );
} }
@override
Rect getCaretPrototype(TextPosition position) {
final child = childAtPosition(position);
final localPosition = TextPosition(
offset: position.offset - child.getContainer().offset,
affinity: position.affinity,
);
return child.getCaretPrototype(localPosition);
}
} }
class _EditableBlock extends MultiChildRenderObjectWidget { class _EditableBlock extends MultiChildRenderObjectWidget {

@ -387,7 +387,7 @@ class RenderEditableTextLine extends RenderEditableBox {
EdgeInsets? _resolvedPadding; EdgeInsets? _resolvedPadding;
bool? _containsCursor; bool? _containsCursor;
List<TextBox>? _selectedRects; List<TextBox>? _selectedRects;
Rect? _caretPrototype; late Rect _caretPrototype;
final Map<TextLineSlot, RenderBox> children = <TextLineSlot, RenderBox>{}; final Map<TextLineSlot, RenderBox> children = <TextLineSlot, RenderBox>{};
Iterable<RenderBox> get _children sync* { Iterable<RenderBox> get _children sync* {
@ -501,8 +501,11 @@ class RenderEditableTextLine extends RenderEditableBox {
} }
bool containsCursor() { bool containsCursor() {
return _containsCursor ??= textSelection.isCollapsed && return _containsCursor ??= cursorCont.isFloatingCursorActive
line.containsOffset(textSelection.baseOffset); ? line
.containsOffset(cursorCont.floatingCursorTextPosition.value!.offset)
: textSelection.isCollapsed &&
line.containsOffset(textSelection.baseOffset);
} }
RenderBox? _updateChild( RenderBox? _updateChild(
@ -638,6 +641,17 @@ class RenderEditableTextLine extends RenderEditableBox {
cursorCont.style.height ?? cursorCont.style.height ??
preferredLineHeight(const TextPosition(offset: 0)); preferredLineHeight(const TextPosition(offset: 0));
// TODO: This is no longer producing the highest-fidelity caret
// heights for Android, especially when non-alphabetic languages
// are involved. The current implementation overrides the height set
// here with the full measured height of the text on Android which looks
// superior (subjectively and in terms of fidelity) in _paintCaret. We
// should rework this properly to once again match the platform. The constant
// _kCaretHeightOffset scales poorly for small font sizes.
//
/// On iOS, the cursor is taller than the cursor on Android. The height
/// of the cursor for iOS is approximate and obtained through an eyeball
/// comparison.
void _computeCaretPrototype() { void _computeCaretPrototype() {
switch (defaultTargetPlatform) { switch (defaultTargetPlatform) {
case TargetPlatform.iOS: case TargetPlatform.iOS:
@ -655,12 +669,24 @@ class RenderEditableTextLine extends RenderEditableBox {
} }
} }
void _onFloatingCursorChange() {
_containsCursor = null;
markNeedsPaint();
}
// End caret implementation
//
// Start render box overrides
@override @override
void attach(covariant PipelineOwner owner) { void attach(covariant PipelineOwner owner) {
super.attach(owner); super.attach(owner);
for (final child in _children) { for (final child in _children) {
child.attach(owner); child.attach(owner);
} }
cursorCont.floatingCursorTextPosition.addListener(_onFloatingCursorChange);
if (containsCursor()) { if (containsCursor()) {
cursorCont.addListener(markNeedsLayout); cursorCont.addListener(markNeedsLayout);
cursorCont.color.addListener(safeMarkNeedsPaint); cursorCont.color.addListener(safeMarkNeedsPaint);
@ -673,6 +699,8 @@ class RenderEditableTextLine extends RenderEditableBox {
for (final child in _children) { for (final child in _children) {
child.detach(); child.detach();
} }
cursorCont.floatingCursorTextPosition
.removeListener(_onFloatingCursorChange);
if (containsCursor()) { if (containsCursor()) {
cursorCont.removeListener(markNeedsLayout); cursorCont.removeListener(markNeedsLayout);
cursorCont.color.removeListener(safeMarkNeedsPaint); cursorCont.color.removeListener(safeMarkNeedsPaint);
@ -817,8 +845,10 @@ class RenderEditableTextLine extends RenderEditableBox {
CursorPainter get _cursorPainter => CursorPainter( CursorPainter get _cursorPainter => CursorPainter(
editable: _body, editable: _body,
style: cursorCont.style, style: cursorCont.style,
prototype: _caretPrototype!, prototype: _caretPrototype,
color: cursorCont.color.value, color: cursorCont.isFloatingCursorActive
? cursorCont.style.backgroundColor
: cursorCont.color.value,
devicePixelRatio: devicePixelRatio, devicePixelRatio: devicePixelRatio,
); );
@ -873,10 +903,14 @@ class RenderEditableTextLine extends RenderEditableBox {
void _paintCursor( void _paintCursor(
PaintingContext context, Offset effectiveOffset, bool lineHasEmbed) { PaintingContext context, Offset effectiveOffset, bool lineHasEmbed) {
final position = TextPosition( final position = cursorCont.isFloatingCursorActive
offset: textSelection.extentOffset - line.documentOffset, ? TextPosition(
affinity: textSelection.base.affinity, offset: cursorCont.floatingCursorTextPosition.value!.offset -
); line.documentOffset,
affinity: cursorCont.floatingCursorTextPosition.value!.affinity)
: TextPosition(
offset: textSelection.extentOffset - line.documentOffset,
affinity: textSelection.base.affinity);
_cursorPainter.paint( _cursorPainter.paint(
context.canvas, effectiveOffset, position, lineHasEmbed); context.canvas, effectiveOffset, position, lineHasEmbed);
} }
@ -921,6 +955,9 @@ class RenderEditableTextLine extends RenderEditableBox {
} }
markNeedsPaint(); markNeedsPaint();
} }
@override
Rect getCaretPrototype(TextPosition position) => _caretPrototype;
} }
class _TextLineElement extends RenderObjectElement { class _TextLineElement extends RenderObjectElement {

Loading…
Cancel
Save