-
Notifications
You must be signed in to change notification settings - Fork 29.8k
iOS Selection Handle Improvements #157815
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
Conversation
|
the iOS one has this hero like animation when tapping on one of the handles which makes the handle expand to the magnifying glass, can you pull this off? |
|
@MaherSafadii That's pretty cool, I didn't realize that:
It's actually not too far away with @Renzo-Olivares's changes. It's probably doable. No pressure in this PR though! |
9c8c8b9 to
8fd31ea
Compare
| super.paint, | ||
| Offset.zero, | ||
| ); | ||
| } else if (selection!.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.
@justinmc I'm wondering if this change is worth while, i'm not seeing any issues without it. Previously when the selection was collapsed SelectionOverlay would build an empty end handle and RenderEditable would only push a layer for the start handle. Now SelectionOverlay may build and empty end handle or start handle depending on certain conditions.
When the selection is collapsed with no handles being dragged, then build an empty end handle, this case would be alright without this code since the end handle is not built.
When the selection is collapsed with the start handle being dragged, then build an empty end handle, this case would also be fine without this code since the end handle is not built.
When the selection is collapsed with the end handle being dragged, then build an empty start handle, this case might not be fine without this code since we are dragging the end handle and its layer won't be there.
This is also the cause of the failing editable_test.dart test.
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.
My first reaction is to not paint this since it's never going to be visible (it always overlaps the start handle perfectly). There won't be any accessibility implications right?
It looks like the failing test just tests the number of layers painted so it shouldn't affect the decision.
00:52 +1212 ~8 -1: /b/s/w/ir/x/w/flutter/packages/flutter/test/rendering/editable_test.dart: RenderEditable.paint respects offset argument [E]
Expected: an object with length of <1>
Actual: [
LeaderLayer:LeaderLayer#e5a4a DETACHED(handles: 2, offset: Offset(100.0, 220.0), link: LayerLink#9f416(<linked>)),
LeaderLayer:LeaderLayer#8743c DETACHED(handles: 1, offset: Offset(100.0, 220.0), link: LayerLink#d0324(<linked>))
]
Which: has length of <2>
_paintHandleLayers will paint a LeaderLayer
package:matcher expect
package:flutter_test/src/widget_tester.dart 480:18 expect
test/rendering/editable_test.dart 200:5 main.<fn>
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 leaning towards keeping this in for the case of:
When the selection is collapsed with the end handle being dragged, then without the code only the start handle layer is pushed. This seems wrong since the end handle is being dragged and its layer will not be there.
RenderParagraph does something similar (pushes both layers when collapsed). https://github.com/flutter/flutter/blob/ae736e2cc325998df328595e039208d545128265/packages/flutter/lib/src/rendering/paragraph.dart#L3086C1-L3105C6
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.
Sounds good.
|
@MaherSafadii, that is a cool behavior. I probably won't get to it in this PR, but will definitely add it to my radar. |
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 with nits 👍 . Can you create an issue for the handle animation (#157815 (comment))?
| super.paint, | ||
| Offset.zero, | ||
| ); | ||
| } else if (selection!.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.
My first reaction is to not paint this since it's never going to be visible (it always overlaps the start handle perfectly). There won't be any accessibility implications right?
It looks like the failing test just tests the number of layers painted so it shouldn't affect the decision.
00:52 +1212 ~8 -1: /b/s/w/ir/x/w/flutter/packages/flutter/test/rendering/editable_test.dart: RenderEditable.paint respects offset argument [E]
Expected: an object with length of <1>
Actual: [
LeaderLayer:LeaderLayer#e5a4a DETACHED(handles: 2, offset: Offset(100.0, 220.0), link: LayerLink#9f416(<linked>)),
LeaderLayer:LeaderLayer#8743c DETACHED(handles: 1, offset: Offset(100.0, 220.0), link: LayerLink#d0324(<linked>))
]
Which: has length of <2>
_paintHandleLayers will paint a LeaderLayer
package:matcher expect
package:flutter_test/src/widget_tester.dart 480:18 expect
test/rendering/editable_test.dart 200:5 main.<fn>
| late double _endHandleDragTarget; | ||
|
|
||
| // The edge to hold in place when the selection is inverted. | ||
| int? _oppositeEdge; |
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: Maybe pivotPoint or something? Not sure if that's any more understandable, up to you...
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.
Or rather than being an integer, should it just indicate whether the start or end handle is being dragged? I feel like the indicator contains information that's duplicated in _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.
Thinking about this some more I think you're right. I think instead of having _oppositeEdge and _dragStartSelectionIsCollapsed, I can just store _dragStartSelection and use it directly. I think we know what handle is being dragged depending on what method we are in _handleSelectionEndHandleDragUpdate and _handleSelectionStartHandleDragUpdate.
Thinking out loud about some of the behaviors:
End handle:
When the selection is normalized and we drag the end handle, for the duration of the drag we can hold the baseOffset in place at the base.
When the selection is inverted and we drag the end handle, for the duration of the drag we can hold the extentOffset in place at the base.
Start handle:
When the selection is normalized and we drag the start handle, for the duration of the drag we can hold the extentOffset in place at the base.
When the selection is inverted and we drag the start handle, for the duration of the drag we can hold the baseOffset in place at the base.
Concrete example:
selection format: (baseOffset, extentOffset)
initial selection (end handle at position 10): (5,10)
drag end handle forward (to position 15): (5, 15)
drag end handle backwards & invert (to position 2) - release drag: (5, 2)
new end handle drag forward (to position 8) (end handle is on position 5 and iOS always sets the new offset as the extentOffset) : (2, 8) since the start handle is on position 2, so that position should be held in place while dragging the end handle.
| // The selection doesn't move beyond the left handle. There's always at | ||
| // least 1 char selected. | ||
| expect(controller.selection.extentOffset, 5); | ||
| }); |
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.
Should this run only on non-Apple platforms?
| // The selection inverts moving beyond the left handle. | ||
| expect(controller.selection.baseOffset, 4); | ||
| expect(controller.selection.extentOffset, 2); | ||
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS })); |
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.
And macOS too?
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 set it to run on iOS only since:
- There are no touch devices running macOS afaik, and selection handles can only be dragged with a touch pointer device.
- This case triggers when we drag selection handles on macOS and immediately causes the handles to hide when dragging them, causing the test to fail after the handle is dragged initially since the handle disappears.
flutter/packages/flutter/lib/src/cupertino/text_field.dart
Lines 1121 to 1127 in 74669e4
case TargetPlatform.macOS: case TargetPlatform.linux: case TargetPlatform.windows: if (cause == SelectionChangedCause.drag) { _editableText.hideToolbar(); } }
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 we need to revisit how handles behave on desktop devices, there is a related issue open #108668. By default desktop platforms do not render selection handles right now, their TextSelectionControls explicitly block this. Right now you can provide a TextSelectionControls to TextField that allows the selection handles to render on desktop platforms, but using a mouse to manipulate them does not work since they only except touch pointer events. The case pointed out in #157815 (comment) also prevents selection handles from working properly on desktop platforms since they disappear immediately.
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.
Ok got it. Similarly I think we messed up mouse input on mobile devices for awhile, but it should work just like it does natively on those devices. So macOS is no big deal, but we should solve #108668 for Windows and Linux.
7dda689 to
ef7d70e
Compare
|
(Triage flyby comment) @Renzo-Olivares it looks like some checks are failing. :) |
|
This pull request executed golden file tests, but it has not been updated in a while (20+ days). Test results from Gold expire after as many days, so this pull request will need to be updated with a fresh commit in order to get results from Gold. For more guidance, visit Writing a golden file test for Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing. |
ef7d70e to
2d7f8d8
Compare
|
The tests are fixed, but thinking about this more I wonder if this is the correct behavior. After this PR on iOS devices when providing any selection handles, whether Cupertino or Material, swapping handles is enabled. On other platforms selection handles does not allow for handle swapping at all. Just taking into account native behavior, I'm leaning towards only edit: This may be more involved since |
|
Golden file changes have been found for this pull request. Click here to view and triage (e.g. because this is an intentional change). If you are still iterating on this change and are not ready to resolve the images on the Flutter Gold dashboard, consider marking this PR as a draft pull request above. You will still be able to view image results on the dashboard, commenting will be silenced, and the check will not try to resolve itself until marked ready for review. For more guidance, visit Writing a golden file test for Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing. |
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.
We talked offline and I advocated keeping the PR as-is, so that the behavior is dependent on the platform and not on the visual handle style, but I'm open for a way to modify the behavior like swapHandles in the future.
Renewing my LGTM 👍
bd0fef7 to
f1fc476
Compare
|
Golden file changes are available for triage from new commit, Click here to view. For more guidance, visit Writing a golden file test for Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing. |
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 👍
Thanks for doing this, it seems like it will greatly improve the look and feel here.
| } | ||
|
|
||
| final TextSelection newSelection; | ||
| final bool dragStartSelectionNormalized = _dragStartSelection!.extentOffset >= _dragStartSelection!.baseOffset; |
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 use _dragStartSelection.isNormalized? Here and below.
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.
isNormalized seemed to behave a bit strangely to me, maybe TextSelection should have an override for it, but the default implementation checks end >= start, which will always be true with regards to a TextSelection, since TextSelection passes normalized values to TextRange
flutter/packages/flutter/lib/src/services/text_editing.dart
Lines 20 to 23 in 5621a5b
| }) : super( | |
| start: baseOffset < extentOffset ? baseOffset : extentOffset, | |
| end: baseOffset < extentOffset ? extentOffset : baseOffset, | |
| ); |
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 ok, keeping it as-is is fine then. I think I've run into the same thing before. Maybe add a comment to avoid someone else coming in and moving this to _dragStartSelection.isNormalized if you think it might help.
| ); | ||
| if (position.offset <= _selection.start) { | ||
| return; // Don't allow order swapping. | ||
| if (dragStartSelectionNormalized) { |
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 could also remove this if/else and just do a ternary for the baseOffset. Either way is fine though.
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.
Done.
| super.paint, | ||
| Offset.zero, | ||
| ); | ||
| } else if (selection!.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.
Sounds good.
| ? TextSelectionHandleType.right | ||
| : TextSelectionHandleType.left, |
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: Would it be easier to read if you indented these two lines? Same for below.
Or you could always do a switch, but I'm not sure if that would help with readability or not here.
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 the switch better 48f01fc (#157815) .

Fixes #110306
Simulator.Screen.Recording.-.iPhone.16.Pro.-.2024-10-29.at.10.43.14.mp4
This change:
TextField.SelectableRegion/SelectionArea, previously they showed both left and right handles when collapsed, instead of the collapsed handles.SelectionOverlay:TextSelectionOverlay:_oppositeEdgeused to maintain selection as handles invert.RenderParagraph:SelectionGeometry, previously we wouldn't so the collapsed state would show both start and end handles.CupertinoTextMagnifier:CupertinoTheme.of(context).primaryColor. Selection handles also usesprimaryColor.Pre-launch Checklist
///).