Repository navigation
Add TextField triple tap/click gestures - #119046
Conversation
There was a problem hiding this comment.
Perhaps remove the doc comment reference to [int] as it's a generic type? Can follow the style of TextBoundary (or even CharacterBoundary which only has class level doc comments...).
There was a problem hiding this comment.
I guess these magic numbers could be avoided with constants (also needed in isLineTerminator).
There was a problem hiding this comment.
Same as above on constants.
There was a problem hiding this comment.
Looking at TextPainter, the list of what's considered a newline is different: no 0xD on its own, plus 0x2028/0x2029.
static bool _isNewline(int codePoint) {
switch (codePoint) {
case 0x000A:
case 0x0085:
case 0x000B:
case 0x000C:
case 0x2028:
case 0x2029:
return true;
default:
return false;
}
}
There was a problem hiding this comment.
Hmm I got the list from here https://www.unicode.org/standard/reports/tr13/tr13-5.html. I added 0x2028 and 0x2029 I had left out.
|
@tgucio sorry this wasn't ready for review. This depends on another PR so some files are not related to this one. Appreciate the feedback nonetheless :) |
|
@Renzo-Olivares this is a very welcome PR so I thought I'd share review comments early but it looks like I missed the "Draft" status :). |
|
This is ready for an initial review with a caveat: I'm still addressing an edge case where the |
justinmc
left a comment
There was a problem hiding this comment.
LGTM with nits 👍
The main thing is just a TODO that I commented on, not sure if you plan to do that in this PR or a new one.
It's really great that this PR is so straightforward with the addition of your paragraph boundary work and refactoring. Before that it was such a mess trying to support triple tap!
There was a problem hiding this comment.
Why do iOS and Mac fall through to the same behavior as Windows if the comments above seem to describe their behavior differently?
There was a problem hiding this comment.
To me it looked like iOS/macOS/Windows increased their tap count infinitely but they handled taps past 3 differently.
iOS and macOS don't handle anything past a third tap. The selection is retained at the paragraph that was selected on the triple tap.
Windows alternates between double click and triple click when going past the third tap. Alternating between selecting the word or paragraph at the position.
Let me know if I can clarify this further.
There was a problem hiding this comment.
I think it's ok as-is. I looked through the code to see how consecutiveTapCount is used and it makes sense that these all return null but later have different behavior.
There was a problem hiding this comment.
I think it makes more sense now after introducing an effectiveConsecutiveTapCount.
There was a problem hiding this comment.
Ah I guess some tests that previously tapped very soon after a double tap might behave differently now. We should watch out for this breaking Google tests or anything else. Though it looks like Google tests are currently passing 👍
There was a problem hiding this comment.
Yeah, how it worked previously is that the double tap timer on TextSelectionGestureDetector would reset the count to 0 after every double tap for every platform. So some test behavior changed a bit, but google tests look good so far.
There was a problem hiding this comment.
Is your plan to do this in another PR?
There was a problem hiding this comment.
I'm hoping that I can fix it in this one. But if the fix becomes overly complex then I'll probably make it into another PR.
There was a problem hiding this comment.
Just wanted to draw attention to this again in case you forgot about it.
There was a problem hiding this comment.
It's a bummer that we have to duplicate all of these tests for Cupertino and Material, but I guess they won't work on EditableText because of the code changes in text_selection.dart.
Someday we need to make it easier to share these kinds of tests!
There was a problem hiding this comment.
Agreed, it would definitely help with velocity if we only had to write these tests once.
There was a problem hiding this comment.
It's nice that this test still works (with minor changes) and makes a lot of sense with the addition of triple tap.
There was a problem hiding this comment.
nit: maybe it's easier to write the if clauses, if the local variable is for "effective tap count".
There was a problem hiding this comment.
or maybe move the logic to the static method you created later in this file that turns the raw tap count to the effective tap count.
There was a problem hiding this comment.
Sorry i'm not sure I follow, can you elaborate? The static method I created later in the file is meant to tell the gesture recognizer what its ceiling is for the consecutive tap count before it resets it to 0.
There was a problem hiding this comment.
is it possible to repurpose _getDefaultMaxConsecutiveTap so it takes the raw consecutive tap count and returns the "effective" tap count? For instance for windows the conversion would look like input < 2 ? input : 2 + input % 2 and for other platforms it's max(input, 3).
There was a problem hiding this comment.
In this scenario if we are repurposing _getDefaultMaxConsecutiveTap are we then setting the ceiling for consecutive tap count on the recognizer to null for all platforms?
In that case I think we can do this.
static int _getEffectiveConsecutiveTapCount(int rawCount) {
switch(defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
return rawCount <= 3 ? rawCount : 0 + (rawCount % 3 == 0 ? 3 : rawCount % 3);
case TargetPlatform.iOS:
case TargetPlatform.macOS:
return math.min(rawCount, 3);
case TargetPlatform.windows:
return rawCount < 2 ? rawCount : 2 + rawCount % 2;
}
}Definitely makes the if cases much better! Thanks for the code and suggestion!
There was a problem hiding this comment.
If selectParagraph API simply calls selectParagraphsInRange API then I think we should have just one method exposed doing both, unless one of them does something specific using a private field like selectWord / selectWordsInRange in editable.dart.
Also, shouldn't this be in editable.dart together with other select* methods?
There was a problem hiding this comment.
I think I want to avoid adding more methods to RenderEditable that mutate the selection, and instead have it provide selection calculations that EditableText uses to mutate its selection.
There was a problem hiding this comment.
For design clarity they should all be in a single place I think. It makes sense to move the selection modifying methods to EditableText but it looks like they were in RenderEditable for years so it'd be a breaking change. Wondering if @LongCatIsLooong or @justinmc may have some thoughts?
There was a problem hiding this comment.
I really like the idea of separating the calculation of text related stuff from its mutation. I hope we can get to where we have a clear place for each. For example, if some Flutter dev wants to create a button that selects the nearest 3 paragraphs or something, it would be nice to be able to give them access to our paragraph selection logic without making them actually make a change to an EditableText.
For now though it looks like Renzo has made selectParagraphsInRange and selectParagraph private and moved them to text_selection.dart. That's definitely fine for now since we can always change it later.
|
Tests currently failing due to #120629 |
|
Friendly bump, still looking for a review. The failing tests should clear up when flutter-team-archive/engine#39693 is merged. |
There was a problem hiding this comment.
Nit: maybe a bit easier to understand like this:
final TextSelection newSelection = isFromParagraphBeforeToParagraph
? TextSelection(baseOffset: fromRange.start, extentOffset: toRange.end),
: TextSelection(baseOffset: fromRange.end, extentOffset: toRange.start);
There was a problem hiding this comment.
Also, wondering if we may ever get weird cases here e.g. both ranges starting at the same offset but with different lengths, overlapping ranges, empty ranges etc.
justinmc
left a comment
There was a problem hiding this comment.
LGTM with nits.
I left one comment about _getEffectiveConsecutiveTapCount.
There was a problem hiding this comment.
Would it make sense to put the logic in _getEffectiveConsecutiveTapCount on the Details classes directly somehow? Looking at these calls, I'm thinking that it would be easy for someone to introduce a bug in the future by forgetting about _getEffectiveConsecutiveTapCount and using consecutiveTapCount directly.
However, if it doesn't make sense for the details to be concerned about this kind of stuff, then I agree with how it is now.
There was a problem hiding this comment.
I can add some comments about this but I think the details classes are platform agnostic at the moment and this particular use of _getEffectiveConsecutiveTapCount is only for text selection gestures.
There was a problem hiding this comment.
Just wanted to draw attention to this again in case you forgot about it.
There was a problem hiding this comment.
indentation nit: tab == 2 spaces I think?
There was a problem hiding this comment.
Is this kind of ternary supposed to be indented 2 or 4 spaces? I think I've done both on different occasions because I wasn't sure...
There was a problem hiding this comment.
I'm actually not too sure, but I just followed the convention used in this file which seems to be 4 spaces.
flutter/packages/flutter/lib/src/widgets/text_selection.dart
Lines 2153 to 2155 in e82dc48
flutter/packages/flutter/lib/src/widgets/text_selection.dart
Lines 2274 to 2275 in e82dc48
There was a problem hiding this comment.
What do you mean by this line?
There was a problem hiding this comment.
Similar explanation here: #119046 (comment)
There was a problem hiding this comment.
nit: !delegate.selectionEnabled to avoid nested ifs (so readers don't have to find the closing bracket).
There was a problem hiding this comment.
Do touch devices work on these platforms?
There was a problem hiding this comment.
I'm not aware of any touch devices on macOS, but there definitely is on Windows.
There was a problem hiding this comment.
Can you remove the 0 + ....? Or maybe 1 + (rawCount - 1) % 3?
There was a problem hiding this comment.
I think I can remove this 0, I'll check on it.
There was a problem hiding this comment.
Is that because the selection control has a tap recognizer? Can you triple tap recognizer declare victory immediately?
There was a problem hiding this comment.
It could but the recognizer never receives the third tap because the selection handles overlay absorb the pointer immediately when they pop up.
There was a problem hiding this comment.
absorb the pointer immediately
Do you mean the text field's hitTest method won't be called, or it will be called, but the selection handle will declare victory immediately in the arena?
There was a problem hiding this comment.
I mean it won't be called, so TextSelectionGestureDetector does not get to participate in the arena at all.
There was a problem hiding this comment.
Were you able to fix this? hitTest should be called for the selection gesture recognizers right?
justinmc
left a comment
There was a problem hiding this comment.
Can you explain again (or link me) why hiding the handles until kDoubleTapTimeout after the double tap wasn't viable? I think this solution is pretty clean, but I want to make sure if we're going to be adding new public API to support it.
Otherwise my comments are pretty small stuff.
| void toggleSelectionHandleOverlayGestureHandling() { | ||
| _selectionOverlay ??= _createSelectionOverlay(); | ||
| _selectionOverlay!.handlesAllowPointers = !_selectionOverlay!.handlesAllowPointers; | ||
| _selectionOverlay!.rebuildHandles(); | ||
| } |
There was a problem hiding this comment.
Nit: Maybe _selectionOverlay should call rebuildHandles itself when handlesAllowPointers changes? I'm thinking about how to best architect this to avoid problems like forgetting to call rebuild in the future.
There was a problem hiding this comment.
That makes sense to me since this method won't really work without a rebuild.
| // The consecutive tap tracker has reset. | ||
| // | ||
| // This triggers when the timer between two taps has elapsed, or when | ||
| // the previous pointers buttons does not match the most recent pointers |
There was a problem hiding this comment.
Also should "pointers" be "pointers'" (apostrophe for possession)?
There was a problem hiding this comment.
I think it might actually be "pointer's" I think "pointers'" implies multiple pointers buttons? Not 100% sure though.
| // tracked in [addAllowedPointer]. | ||
| double? get slopTolerance; | ||
|
|
||
| // The consecutive tap tracker has reset. |
There was a problem hiding this comment.
Nit: Maybe "Called when the consecutive..."
| SchedulerBinding.instance.addPostFrameCallback((Duration duration) { | ||
| _buildScheduled = false; | ||
| // If the current editableText state is not available we should | ||
| // exit out. This can occur at the end of tests, or when the |
There was a problem hiding this comment.
Nit: "exit out" => "return".
| } | ||
|
|
||
| /// {@macro flutter.widgets.SelectionOverlay.rebuildHandles} | ||
| void rebuildHandles() { |
There was a problem hiding this comment.
Above I suggested maybe calling this on a setter instead of from EditableText. If so, then maybe we wouldn't have to expose this new method at all.
| } | ||
| _buildScheduled = true; | ||
| SchedulerBinding.instance.addPostFrameCallback((Duration duration) { | ||
| _buildScheduled = false; |
There was a problem hiding this comment.
Is there any need to check if you've been disposed here in this callback too?
There was a problem hiding this comment.
I'm unsure about this. I added an assert(context.mounted) in _rebuildHandles. I see that's what done in some methods like showToolbar.
There was a problem hiding this comment.
Is this kind of ternary supposed to be indented 2 or 4 spaces? I think I've done both on different occasions because I wasn't sure...
I thought the delay was a bit too noticeable, especially since the highlight pops up on double tap as well. Here is the repo with how that would look like https://github.com/Renzo-Olivares/flutter/commits/triple-tap-select-delay-handles, this might require some more specifics but this was a quick POC. It's on top of this PR but I disabled all of the ignore-pointer related functionality. The delay is not noticeable on subsequent double taps.
|
This PR adds the following triple tap/click gestures to select text in a TextField to match the native platform behavior.
Fixes #63576
///).