-
Notifications
You must be signed in to change notification settings - Fork 29.7k
Reland "Single tap on the previous selection should toggle the toolbar on iOS #108913" #111995
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
Reland "Single tap on the previous selection should toggle the toolbar on iOS #108913" #111995
Conversation
2c4ef21 to
90cabbd
Compare
…e toolbar on iOS…" (flutter#111662)" This reverts commit 61db3bc.
| position, | ||
| ); | ||
|
|
||
| return renderEditable.selection!.start < textPosition.offset |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
when offset == start why is it not considered not on selection?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah I saw the comment from the previous pull request. But you may still want to check the affinity of the incoming position?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What should the TextAffinity be checked for?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh nvm you don't have to since the logic will be identical.
| } | ||
|
|
||
| bool _tapWasOnSelection(Offset position) { | ||
| if (renderEditable.selection == null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
consider creating a local variable for selection so you can promote it to non null.
| void toggleToolbar([bool hideHandles = true]) { | ||
| if (_selectionOverlay == null) { | ||
| _createSelectionOverlay(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
final selectionOverlay = _selectionOverlay ??= createSelectionOverlay() to avoid the force cast?
| renderEditable.selectWordEdge(cause: SelectionChangedCause.tap); | ||
| } | ||
| if (previousSelection == editableText.textEditingValue.selection && renderEditable.hasFocus) { | ||
| editableText.toggleToolbar(false); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could this be combined with the previous if clause?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if it can, but I'll try it out.
justinmc
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM 👍
|
|
||
| /// Toggles the visibility of the toolbar. | ||
| void toggleToolbar() { | ||
| assert(_selectionOverlay != null); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know why we didn't create the selection overlay here in the first place. Seems fine to me.
|
|
||
| void _createSelectionOverlay() { | ||
| _selectionOverlay = TextSelectionOverlay( | ||
| TextSelectionOverlay _createSelectionOverlay() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I like this pattern better 👍
| ); | ||
|
|
||
| return selection.start < textPosition.offset | ||
| && selection.end > textPosition.offset; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not inclusive?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I updated the logic.
justinmc
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Renewing my LGTM after the previous changes 👍
| await tester.tapAt(textfieldStart + const Offset(150.0, 9.0)); | ||
| await tester.pump(const Duration(milliseconds: 50)); | ||
| // First tap moved the cursor. | ||
| // First tap moved the cursor and hides the toolbar. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"hides" => "hid"
| // If the tap was neither on the selection exclusively or inclusively then we select the word edge. After | ||
| // selecting the word edge, if our selection remains the same, then we toggle the toolbar. If the selection changes | ||
| // then we hide the toolbar. | ||
| if (((_tapWasOnSelectionExclusive(details.globalPosition) && !previousSelection.isCollapsed) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a bit hard to read. Could you simplify this a bit?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also you probably don't need _tapWasOnSelectionInclusive? if previousSelection == editableText.textEditingValue.selection then we'll toggle the toolbar anyway?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think there is still the case where the cursor is in the middle of a word wor|d, and we tap it.
If we remove the inclusive check, then the exclusive one will not catch it, and the cursor will move to the word edge.
The expected behavior would be to toggle the toolbar when we tap the cursor.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if () {
// If the tap was on the selection (different meaning for collapsed vs uncollapsed selection)
// and the editable was focused, then toggle the toolbar.
} else if () {
// Tap was not on the selection at all.
// We should still toggle the toolbar, if the selection remains the same after the word edge is selected.
// If the selection changes then we hide the toolbar.
}There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I updated the logic a little, getting rid of the else if.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I’ll keep a lookout of affinity issues.
On native if the renderEditable doesn’t have focus we always select the closest word edge. And the toolbar is not toggled unless the editable has focus.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you check if you tap the latter half of the character before the caret? I think that will give you (x-1, upstream) instead of (x, downstream) so wasTapOnSelection will return false? But that should also toggle the toolbar right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point, If i do the later half of the character before the character the selection is upstream. If I then follow that up with a tap on the former half of the character after the caret, then that selection is downstream. Then because of previousSelection == editableText.textEditingValue.selection, the toolbar is not toggled in this case. On native it looks like it toggles the toolbar regardless of the affinity.
In the case where the text is wrapping, then if we place the cursor at the end of the first line, and then try to tap at the beginning of the second line, then the toolbar will be toggled because wasTapOnSelectionInclusive returns true.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think in the case where we select the word edge the affinity does not matter since a word will not be wrapped between two lines (will either fall on first or second line).
In the case where we check for wasTapOnSelectionInclusive we do care about the affinity because we want to be able to select the text at beginning of the next line. So we want it to return false when the affinity is different.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There might be a trade off we have to make here since we cannot differentiate why the TextAffinity changed.
We want to toggle the toolbar when
- We call
selectWordEdgeand the selection after the call is the same as the previous selection (including TextAffinity). - We call
selectWordEdgeand the selection after the call is the same as the previous selection (excluding TextAffinity), this can happen when we tap
We want to hide the toolbar when
- We call
selectWordEdgeand the selection after the call is the same as the previous selection (excluding TextAffinity). This can happen when there is a multi-line text field, there is word wrap and we want to tap on the boundary. Let's say the cursor is at the end of the first line. Tapping on the first line boundary should toggle the toolbar, tapping at the beginning of the second line should hide the toolbar.
We can't differentiate whether TextAffinity changed due to tapping on a new line but same selection offset, or if it occurred from tapping on the same line with the same selection offset.
justinmc
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 I'm on board with the compromise made here, it seems like the best option in the circumstances. But I want to explore this a little farther just to make sure.
You probably already saw this but the logic that gives the affinity based on the side of a character is all the way down in paragraph_txt.cc:
double glyph_center = (gp->x_pos.start + gp->x_pos.end) / 2;
if ((direction == TextDirection::ltr && dx < glyph_center) ||
(direction == TextDirection::rtl && dx >= glyph_center)) {
return PositionWithAffinity(gp->code_units.start, DOWNSTREAM);
} else {
return PositionWithAffinity(gp->code_units.end, UPSTREAM);
}So that probably shouldn't change. Instead, the ideal logic is that on tap, we only care about affinity for toggling the toolbar if the tap is at a soft wordwrap.
Can we know about the location of soft wordwraps at this point in the pipeline? The text has already been laid out since this is a tap callback, and getPositionForPoint (called already on line 1948) requires the text to be laid out too. So the information we need is there, but if I'm not mistaken, there is no good way to get this from RenderEditable.
Tl;dr: The alternative to this compromise
You could add a method to RenderEditable that tells you one way or another if a given TextPosition is at a soft wordwrap or not. I think you can figure this out using LineMetrics. If the tap is at a soft wordwrap, only toggle the toolbar if the TextAffinity matches. If it's not at a soft wordwrap, toggle it regardless of the affinity.
Curious about your thoughts on if this is a correct analysis, and if so if it's worth it or not! But I'm on board with this PR either way. Maybe this could even be done in a follow-up PR.
| // toggle the toolbar. If the selection changes then we hide the toolbar. | ||
| final TextSelection previousSelection = renderEditable.selection ?? editableText.textEditingValue.selection; | ||
| final bool isAffinityTheSame = renderEditable.getPositionForPoint(details.globalPosition).affinity == previousSelection.affinity; | ||
| if (((_tapWasOnSelectionExclusive(details.globalPosition) && !previousSelection.isCollapsed) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: You're potentially calling getPositionForPoint 3 times. The call is via FFI so could be a bit costly than usual dart calls. So maybe store that in a local variable and that might help simplify the code too.
I 100% agree with your analysis. I will investigate further on how feasible adding such an API to renderEditable is, but it definitely sounds worth it to add if its possible. That will be in a separate PR. I created an issue for this #113228. |
…e toolbar on iOS #108913" (flutter/flutter#111995)
…e toolbar on iOS #108913" (flutter/flutter#111995)
…e toolbar on iOS #108913" (flutter/flutter#111995)
…e toolbar on iOS #108913" (flutter/flutter#111995)
…e toolbar on iOS flutter#108913" (flutter#111995)" This reverts commit 4abe6fd.
Relands #108913
Fixes #111660
Was failing due to a call to EditableText.toggleToolbar which does a forced null dereference on SelectionOverlay. There is an assert that checks if the SelectionOverlay exists, but this does not work on production. This change creates a SelectionOverlay if it does not exist when toggleToolbar is called and adds a regression test.
Pre-launch Checklist
///).