Skip to content

Add double click and double click + drag gestures to SelectionArea - #124817

Merged
auto-submit[bot] merged 66 commits into
flutter:masterfrom
Renzo-Olivares:selection-area-tapdrag
Aug 11, 2023
Merged

auto-submit[bot] merged 66 commits into
flutter:masterfrom
Renzo-Olivares:selection-area-tapdrag

Conversation

@Renzo-Olivares

@Renzo-Olivares Renzo-Olivares commented Apr 13, 2023 •

Copy link
Copy Markdown
Contributor

Adds double click to select a word.
Adds double click + drag to select word by word.

Screen.Recording.2023-04-25.at.11.03.35.AM.mov

Partially fixes #104552

Pre-launch Checklist

  • I read the [Contributor Guide] and followed the process outlined there for submitting PRs.
  • I read the [Tree Hygiene] wiki page, which explains my responsibilities.
  • I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement].
  • I signed the [CLA].
  • I listed at least one issue that this PR fixes in the description above.
  • I updated/added relevant documentation (doc comments with ///).
  • I added new tests to check the change I am making, or this PR is [test-exempt].
  • All existing and new tests are passing.

@flutter-dashboard flutter-dashboard Bot added the framework flutter/packages/flutter repository. See also f: labels. label Apr 13, 2023
@Renzo-Olivares Renzo-Olivares changed the title [WIP] Add double tap + double tap and drag gestures to SelectionArea Add double tap + double tap and drag gestures to SelectionArea Apr 25, 2023
@flutter-dashboard flutter-dashboard Bot added the p: material_ui material_ui package in flutter/packages label Apr 25, 2023
@Renzo-Olivares
Renzo-Olivares marked this pull request as ready for review April 25, 2023 17:59
@Renzo-Olivares Renzo-Olivares changed the title Add double tap + double tap and drag gestures to SelectionArea Add double click + double click and drag gestures to SelectionArea Apr 25, 2023
@Renzo-Olivares Renzo-Olivares changed the title Add double click + double click and drag gestures to SelectionArea Add double click and double click and drag gestures to SelectionArea Apr 25, 2023

@justinmc justinmc left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM with nits 👍

I'm always bummed that we don't get this kind of behavior for free shared between RenderEditable and RenderParagraph. But given that we can't do that yet, this is pretty clean, and I think TextGranularity makes it straightforward.

You have some test failures FYI.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow pattern matching 🤩

Comment on lines 391 to 392

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these two names be the same? textGranularity and granularity.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we have a method with the same name in text_selection.dart. Is it reasonable to share code here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I can re-visit this once I implement triple-click gestures. Right now there are some small nuances between how this effective consecutive tap count works on static vs editable text.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No apostrophe here: "platform's" => "platforms"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same for below.

Comment on lines 513 to 517

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return isPositionAtOrBelowStartingBaseline;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should there be spaces between the brackets and the text? I forget. Here and below, if I'm right.

@Renzo-Olivares Renzo-Olivares Apr 25, 2023 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like the other methods in this file with named parameters do not, so I just followed that. But I am also unsure what the preferred styling here is.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds like it's not a big deal then, keeping it like this is fine 👍

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was probably my oversight.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should textGranularity actually receive a default value here?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Maybe specify that the default is [TextGranularity.character] instead of saying "character by character".

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this needed now? (and in a few other places below)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is needed because of the switch to PanGestureRecognizer -> TapAndPanGestureRecognizer. PanGestureRecognizer calls onDown earlier than TapAndPanGestureRecognizer. PanGestureRecognizer calls it in addAllowedPointer, and TapAndPanGestureRecognizer waits for the recognizer to declare victory.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the explanation!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Super nit: Maybe assign this offset to a variable so that it's clear where it's coming from?

@Renzo-Olivares Renzo-Olivares changed the title Add double click and double click and drag gestures to SelectionArea Add double click and double click + drag gestures to SelectionArea Apr 25, 2023
@Renzo-Olivares

Renzo-Olivares commented Apr 26, 2023 •

Copy link
Copy Markdown
Contributor Author

Looks like the failing tests are because getPositionForOffset returns a different value on web vs other platforms.

  Offset textOffsetToPosition(RenderParagraph paragraph, int offset) {
    const Rect caret = Rect.fromLTWH(0.0, 0.0, 2.0, 20.0);
    final Offset localOffset = paragraph.getOffsetForCaret(TextPosition(offset: offset), caret);
    return paragraph.localToGlobal(localOffset);
  }

  test('getWordBoundary control test failing', () {
    final RenderParagraph paragraph = RenderParagraph(
      const TextSpan(text: 'How are you?'),
      textDirection: TextDirection.ltr,
    );
    layout(paragraph);

    final Offset target4 = textOffsetToPosition(paragraph, 4);
    final TextPosition position4 = paragraph.getPositionForOffset(target4);
    final TextRange range4 = paragraph.getWordBoundary(position4);
    expect(range4.textInside('How are you?'), equals('are'));
  });

This fails with --platform=chrome because web getPositionForOffset returns TextPosition(4, upstream) and other platforms return TextPosition(4, downstream).

Looks like this test passes using --web-renderer=canvaskit but fails for skwasm and html.

Opened #125582 for further investigation.

@justinmc

Copy link
Copy Markdown
Contributor

Thanks for opening that issue!


final (TextPosition start, TextPosition end) wordBoundary = _getWordBoundaryAtPosition(position);
TextPosition targetPosition = isEnd ? wordBoundary.$2 : wordBoundary.$1;
if (!_rect.contains(localPosition)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not entirely sure what this is for can you explain?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found that since SelectionUtils.adjustDragOffset returns the top left of the rect for ltr if the drag is in Area1 of a Selectable it will return TextPosition(0). When using _getWordBoundaryAtPosition(position) that ends up selecting the first word of the selectable.

This is not desired since say we have a list of Selectables and our drag starts at the first one, dragging down to the second one moves the selection to the word on the second selectable, but then when dragging back up to the first selectable the second selectables first word is still selected because _getWordBoundaryAtPosition(TextPosition(0)) = first word.

This same logic is in _updateSelectionEdge but it works there because the edge will moved to TextPosition(0) and not the word at TextPosition(0).

This was my workaround for this issue.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for explanation. If this is the case, I think you can move this before the whole _getWordBoundaryAtPosition and short circuit it. Also would be good to add some comments about this

localPosition,
direction: paragraph.textDirection,
);
final TextPosition position = _clampTextPosition(paragraph.getPositionForOffset(adjustedOffset));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This probably should instead clamp after _getWordBoundaryAtPosition

it is fine if the position is outside of the text range of this selectablefragment, as long as the targetPosition is in the text range.

return SelectionResult.previous;
}

return SelectionUtils.getResultBasedOnRect(_rect, localPosition);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you also copy the todo over?

/// The [globalPosition] contains the new offset of the edge.
///
/// This event is dispatched when the framework detects [DragStartDetails] in
/// The [granularity] contains the granularity that the selection edge should move by.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we document that only character and word is supported?

Comment thread packages/flutter/lib/src/widgets/selectable_region.dart Outdated
@flutter-dashboard flutter-dashboard Bot added the f: scrolling Viewports, list views, slivers, etc. label Apr 29, 2023

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do you need to cache this value? is it possible to figure out whether the you need to move the start or end based on the current selection and the newly updated selection? for example, if current selection is (start: 3, end: 6) and you receive a end update, you will know you need to swap the start if new end position is < 3

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is hard to read the logic in these block. Can you add check isEnd in the condition? I think it would be noop in the set selection anyway

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you don't need to cache the _originWordSelectionStart, you just need to find the wordboundary of _textSelectionEnd/Start and move them to the other side of the word.

Also the current logc assume _originWordSelectionStart and _originWordSelectionEnd are enclosing a word. is it possible it is not the case?

Comment thread packages/flutter/lib/src/rendering/selection.dart Outdated
Comment thread packages/flutter/lib/src/rendering/selection.dart Outdated
@github-actions github-actions Bot added f: scrolling Viewports, list views, slivers, etc. framework flutter/packages/flutter repository. See also f: labels. and removed f: scrolling Viewports, list views, slivers, etc. framework flutter/packages/flutter repository. See also f: labels. labels May 30, 2023
@Renzo-Olivares

Copy link
Copy Markdown
Contributor Author

This is ready for another review. I'm still running into the issue where the scrollable synthesizes an event for the opposite edge sometimes losing the origin word boundary, but this happens a lot less if we track the synthesized event 9239d36.

Comment thread packages/flutter/lib/src/rendering/paragraph.dart Outdated
Comment thread packages/flutter/lib/src/rendering/paragraph.dart Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should the rect check here also applies to handle select word? if so we could consider put all these tweak into _getWordBoundaryAtPosition

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This rect check is to handle the results of SelectionUtils.adjustDragOffset which isn't used in handle select word.

Comment thread packages/flutter/lib/src/rendering/paragraph.dart Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to be inverse of shouldSwapEdgesWhenSelectionIsNormalized except for the equal case? can the equal case be handle the same way as either one of shouldSwapEdgesWhenSelectionIsNormalized or shouldSwapEdgesWhenSelectionNotNormalized

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is also hard to interpret the name. is it swaping the existingSelectionStart and existingSelectionEnd or is it swaping the targetPosition to be wordBoundary.wordStart or wordBoundary.wordEnd? base on the code, it seems to be both.

Maybe call this willNewSelectionBeNormalized

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The equal case should actually not swap, I have updated the implementation to reflect this.

Comment thread packages/flutter/lib/src/rendering/paragraph.dart Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which class is synthesizing the start event to selectable?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_startNewMouseSelectionGesture is the one setting the start edge with _selectWordAt, or were you referring to something else?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am referring to cases where the _selectEndTo reach a new selectable other than the _selectWordAt originally sent to

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_SelectableRegionContainerDelegate is synthesizing the first start event on ensureChildUpdated when _selectEndTo reaches a new selectable.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, so it would be a selectStart with granularity character. As long as renderParagraph can handle that, that sounds fine.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

watch out for ellipsized case where the targetPosition may not be able to reach range.end. basically what to todo is describing below.

The return value should always be determine by SelectionUtils.getResultBasedOnRect(_rect, localPosition); until we have a way to know what text are ellipsized

Comment thread packages/flutter/lib/src/rendering/paragraph.dart Outdated
doowonee added a commit to doowonee/dlog that referenced this pull request Jul 23, 2023
아직 더블클릭시 단어 선택 기능은 없음 아래 링크 merge 되면 될듯?
flutter/flutter#124817

파비콘 변경해보고 배경색 정도 변경해봄

근데 자체 tag가 생성되서 이게 SEO가 잘되나? 표준 태그들 안쓰는거
같은데

@justinmc justinmc left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly just questions about code organization.

Comment on lines 1532 to 1534

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Is this any simpler?

final (wordStart: TextPosition wordStart, wordEnd: TextPosition wordEnd) = _getWordBoundaryAtPosition(edgeToKeepInPlace);

So you don't have to refer to localWordBoundary. Here and other places where you receive a record.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mentioned this before in my old review but just to double check, it's not possible to deduplicate this code with what we have in EditableText is it? Assuming that EditableText also has similar code to extend selection by word.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code for extending selection by word is in RenderEditable here

/// Selects the set words of a paragraph that intersect a given range of global positions.
///
/// The set of words selected are not strictly bounded by the range of global positions.
///
/// The first and last endpoints of the selection will always be at the
/// beginning and end of a word respectively.
///
/// {@macro flutter.rendering.RenderEditable.selectPosition}
void selectWordsInRange({ required Offset from, Offset? to, required SelectionChangedCause cause }) {
_computeTextMetricsIfNeeded();
final TextPosition fromPosition = _textPainter.getPositionForOffset(globalToLocal(from - _paintOffset));
final TextSelection fromWord = getWordAtOffset(fromPosition);
final TextPosition toPosition = to == null ? fromPosition : _textPainter.getPositionForOffset(globalToLocal(to - _paintOffset));
final TextSelection toWord = toPosition == fromPosition ? fromWord : getWordAtOffset(toPosition);
final bool isFromWordBeforeToWord = fromWord.start < toWord.end;
_setSelection(
TextSelection(
baseOffset: isFromWordBeforeToWord ? fromWord.base.offset : fromWord.extent.offset,
extentOffset: isFromWordBeforeToWord ? toWord.extent.offset : toWord.base.offset,
affinity: fromWord.affinity,
),
cause,
);
}
. I think the main difference is that we are not using the drag origin position to hold the origin word in place, but also a selection in SelectionArea can span multiple Selectables while one EditableText contains the entire selection so the implementation details are a little different.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's really hard to follow all of the if/else blocks below here. Would it be possible to split any of this up into smaller methods or anything like that?

I do appreciate all of the comments though!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried simplifying some of the cases a bit more. Hope that clears it up a bit.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am also agree, I would suggest separating out two branches (isEnd = true/false) into two separate function. It seems like this code try too hard to deduplicate these two branchs but now it is hard to figure out which edge a variable holds

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've separated the logic into two methods, one for updating the end edge and one for updating the start edge. I think it is clearer just more LOC.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like an improvement to me despite more lines 👍

@Renzo-Olivares
Renzo-Olivares requested a review from chunhtai August 1, 2023 19:13

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, so it would be a selectStart with granularity character. As long as renderParagraph can handle that, that sounds fine.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was probably my oversight.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am also agree, I would suggest separating out two branches (isEnd = true/false) into two separate function. It seems like this code try too hard to deduplicate these two branchs but now it is hard to figure out which edge a variable holds

Comment thread packages/flutter/lib/src/rendering/paragraph.dart Outdated
Comment on lines 1542 to 1543

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
final TextPosition? possibleExistingEdge = existingSelectionStart ?? existingSelectionEnd;
if (possibleExistingEdge != null && (possibleExistingEdge == existingSelectionStart && isEnd || possibleExistingEdge == existingSelectionEnd && !isEnd)) {
final TextPosition? possibleExistingEdge = isEnd ? existingSelectionStart : existingSelectionEnd;
if (possibleExistingEdge != null) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As long as the non-moving edge is null you have to go to the else case no?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also this may be easier if we separate out the isEnd = true/false into two separate functions

engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Aug 13, 2023
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Aug 14, 2023
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Aug 14, 2023
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Aug 14, 2023
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Aug 14, 2023
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Aug 14, 2023
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Aug 14, 2023
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Aug 14, 2023
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Aug 14, 2023
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Aug 15, 2023
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Aug 15, 2023
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Aug 15, 2023
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Aug 15, 2023
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Aug 15, 2023
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Aug 15, 2023
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Nov 15, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

autosubmit Merge PR when tree becomes green via auto submit App f: scrolling Viewports, list views, slivers, etc. framework flutter/packages/flutter repository. See also f: labels. p: material_ui material_ui package in flutter/packages

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SelectionArea does not select the text when using double and triple tap

3 participants