Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Visibility of text selection handlers #533

Merged
merged 4 commits into from Oct 19, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
53 changes: 50 additions & 3 deletions packages/zefyr/lib/src/rendering/editor.dart
Expand Up @@ -97,6 +97,7 @@ abstract class RenderAbstractEditor {
class RenderEditor extends RenderEditableContainerBox
implements RenderAbstractEditor {
RenderEditor({
ViewportOffset? offset,
List<RenderEditableBox>? children,
required NotusDocument document,
required TextDirection textDirection,
Expand Down Expand Up @@ -143,6 +144,19 @@ class RenderEditor extends RenderEditableContainerBox
markNeedsSemanticsUpdate();
}

Offset get _paintOffset => Offset(0.0, -(offset?.pixels ?? 0.0));

ViewportOffset? get offset => _offset;
ViewportOffset? _offset;

set offset(ViewportOffset? value) {
if (_offset == value) return;
if (attached) _offset?.removeListener(markNeedsPaint);
_offset = value;
if (attached) _offset?.addListener(markNeedsPaint);
markNeedsLayout();
}

/// The region of text that is selected, if any.
///
/// The caret position is represented by a collapsed selection.
Expand Down Expand Up @@ -196,8 +210,6 @@ class RenderEditor extends RenderEditableContainerBox
/// visible on the screen.
ValueListenable<bool> get selectionStartInViewport =>
_selectionStartInViewport;

// TODO: implement selectionStartInViewport
final ValueNotifier<bool> _selectionStartInViewport =
ValueNotifier<bool>(true);

Expand All @@ -211,10 +223,44 @@ class RenderEditor extends RenderEditableContainerBox
/// This bool indicates whether the text is scrolled so that the handle is
/// inside the text field viewport, as opposed to whether it is actually
/// visible on the screen.
// TODO: implement selectionEndInViewport
ValueListenable<bool> get selectionEndInViewport => _selectionEndInViewport;
final ValueNotifier<bool> _selectionEndInViewport = ValueNotifier<bool>(true);

void _updateSelectionExtentsVisibility(Offset effectiveOffset) {
final visibleRegion = Offset.zero & size;
final startPosition =
TextPosition(offset: selection.start, affinity: selection.affinity);
final startOffset = _getOffsetForCaret(startPosition);
// TODO(justinmc): https://github.com/flutter/flutter/issues/31495
pulyaevskiy marked this conversation as resolved.
Show resolved Hide resolved
// Check if the selection is visible with an approximation because a
// difference between rounded and unrounded values causes the caret to be
// reported as having a slightly (< 0.5) negative y offset. This rounding
// happens in paragraph.cc's layout and TextPainer's
// _applyFloatingPointHack. Ideally, the rounding mismatch will be fixed and
// this can be changed to be a strict check instead of an approximation.
const visibleRegionSlop = 0.5;
_selectionStartInViewport.value = visibleRegion
.inflate(visibleRegionSlop)
.contains(startOffset + effectiveOffset);

final endPosition =
TextPosition(offset: selection.end, affinity: selection.affinity);
final endOffset = _getOffsetForCaret(endPosition);
_selectionEndInViewport.value = visibleRegion
.inflate(visibleRegionSlop)
.contains(endOffset + effectiveOffset);
}

// returns offset relative to this at which the caret will be painted
// given a global TextPosition
Offset _getOffsetForCaret(TextPosition position) {
amantoux marked this conversation as resolved.
Show resolved Hide resolved
final child = childAtPosition(position);
final childPosition = child.globalToLocalPosition(position);
final boxParentData = child.parentData as BoxParentData;
final localOffsetForCaret = child.getOffsetForCaret(childPosition);
return boxParentData.offset + localOffsetForCaret;
}

/// Finds the closest scroll offset that fully reveals the editing cursor.
///
/// The `scrollOffset` parameter represents current scroll offset in the
Expand Down Expand Up @@ -513,6 +559,7 @@ class RenderEditor extends RenderEditableContainerBox
@override
void paint(PaintingContext context, Offset offset) {
defaultPaint(context, offset);
_updateSelectionExtentsVisibility(offset + _paintOffset);
_paintHandleLayers(context, getEndpointsForSelection(selection));
}

Expand Down
24 changes: 22 additions & 2 deletions packages/zefyr/lib/src/widgets/editor.dart
Expand Up @@ -8,6 +8,7 @@ import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:notus/notus.dart';
import 'package:zefyr/src/widgets/baseline_proxy.dart';
import 'package:zefyr/src/widgets/single_child_scroll_view.dart';

import '../rendering/editor.dart';
import '../services/keyboard.dart';
Expand Down Expand Up @@ -1111,10 +1112,25 @@ class RawEditorState extends EditorState
child = BaselineProxy(
textStyle: _themeData.paragraph.style,
padding: baselinePadding,
child: SingleChildScrollView(
child: ZefyrSingleChildScrollView(
controller: _scrollController,
physics: widget.scrollPhysics,
child: child,
viewportBuilder: (_, offset) => CompositedTransformTarget(
link: _toolbarLayerLink,
child: _Editor(
key: _editorKey,
offset: offset,
document: widget.controller.document,
selection: widget.controller.selection,
hasFocus: _hasFocus,
textDirection: _textDirection,
startHandleLayerLink: _startHandleLayerLink,
endHandleLayerLink: _endHandleLayerLink,
onSelectionChanged: _handleSelectionChanged,
padding: widget.padding,
children: _buildChildren(context),
),
),
),
);
}
Expand Down Expand Up @@ -1210,6 +1226,7 @@ class _Editor extends MultiChildRenderObjectWidget {
_Editor({
required Key key,
required List<Widget> children,
this.offset,
required this.document,
required this.textDirection,
required this.hasFocus,
Expand All @@ -1220,6 +1237,7 @@ class _Editor extends MultiChildRenderObjectWidget {
this.padding = EdgeInsets.zero,
}) : super(key: key, children: children);

final ViewportOffset? offset;
final NotusDocument document;
final TextDirection textDirection;
final bool hasFocus;
Expand All @@ -1232,6 +1250,7 @@ class _Editor extends MultiChildRenderObjectWidget {
@override
RenderEditor createRenderObject(BuildContext context) {
return RenderEditor(
offset: offset,
document: document,
textDirection: textDirection,
hasFocus: hasFocus,
Expand All @@ -1246,6 +1265,7 @@ class _Editor extends MultiChildRenderObjectWidget {
@override
void updateRenderObject(
BuildContext context, covariant RenderEditor renderObject) {
renderObject.offset = offset;
renderObject.document = document;
renderObject.node = document.root;
renderObject.textDirection = textDirection;
Expand Down