-
Notifications
You must be signed in to change notification settings - Fork 28.5k
handleSelectWord in MultiSelectableSelectionContainerDelegate should handle rects inside of rects #127478
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
handleSelectWord in MultiSelectableSelectionContainerDelegate should handle rects inside of rects #127478
Conversation
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 but maybe give @chunhtai a chance to take a look too.
@@ -1892,7 +1923,15 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai | |||
final Rect localRect = Rect.fromLTWH(0, 0, selectables[index].size.width, selectables[index].size.height); | |||
final Matrix4 transform = selectables[index].getTransformTo(null); | |||
final Rect globalRect = MatrixUtils.transformRect(transform, localRect); | |||
if (globalRect.contains(event.globalPosition)) { | |||
final List<Rect> rectsInside = _selectableContains(selectables[index], selectables); |
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.
Is this making this O(n^2) instead of O(n) before? Not sure if this is a bottle neck at all, just pointing it out.
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 this would be O(n^2) now. I couldn't think of a re-factor for the current solution that would make this O(n). @chunhtai do you have any thoughts on this?
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 am not sure if this is the right fix. I thought we may run into the same problem even if the inner selectable is not completely contained by the outer one?
In the example
It seems to me the right behavior is that if the selectword at the inside of inner selectable, the outer box should return SelectionResult.next because it should have known the coordinate is outside its actual text layout.
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.
That makes a lot of sense! That makes the solution a lot simpler and keeps this O(n). Thanks for the input!
5183712
to
3efb197
Compare
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 👍
Good call @chunhtai, this way seems a lot better.
@@ -1478,7 +1478,10 @@ class _SelectableFragment with Selectable, ChangeNotifier implements TextLayoutM | |||
assert(word.isNormalized); | |||
// Fragments are separated by placeholder span, the word boundary shouldn't | |||
// expand across fragments. | |||
assert(word.start >= range.start && word.end <= range.end); | |||
final bool wordInsideSelectableBounds = word.start >= range.start && word.end <= range.end; |
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 should only be three situation
- both word.end and word.start is smaller than range.start
- both word.end and word.start is larger than range.end
- range completely cover the word.
(1) should return prev.
(2) should return next.
(3) should return end.
if not in above three case it should throw.
@@ -1894,7 +1894,10 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai | |||
final Rect globalRect = MatrixUtils.transformRect(transform, localRect); |
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.
Since we change the selectword contract that it should return next/prev we also need to update handleSelectWord to return pre/next. i.e. if all selectable return next. or if the selectable return prev.
Also should probably assert if a selectable return prev, it must be the first in the selectables.
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 understand why this is needed but for clarification purposes can you confirm my assumptions. We need to update handleSelectWord
to return next/prev
because before this change it only return none/end
. none
if the word was not selected given the list of selectables managed by the container delegate. And end
if the word was selected selected given the list of selectables
.
Now since select word can return next/prev
, handleSelectWord
should be updated to also do this in the following cases:
(1) return next
when the last SelectionResult
is next
(2) return prev
when the last SelectionResult
is prev
Is this correct?
Also should probably assert if a selectable return prev, it must be the first in the selectables.
- Not sure I completely understand why for this.
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.
none if the word was not selected given the list of selectables managed by the container delegate. And end if the word was selected selected given the list of selectables.
This is correct prior to this change, though the return value was never used so it doesn't really matter what value is return.
Now since select word can return next/prev, handleSelectWord should be updated to also do this in the following cases:
(1) return next when the last SelectionResult is next
(2) return prev when the last SelectionResult is prev
(2) is incorrect, handleSelectWord should only return prev if the selectionResult of first selectable return prev.
Is this correct?
yes this is correct and it is necessary because this selectionContainer can be a child selectable of another container, and it can also have the overlap issue with another selectable.
Not sure I completely understand why for this.
Nvm I think i was wrong, it is possible a selectable that is not the first selectable.
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.
Thank you for the clarification, I think I got it!
for (int index = 0; index < selectables.length; index += 1) { | ||
final Rect localRect = Rect.fromLTWH(0, 0, selectables[index].size.width, selectables[index].size.height); | ||
final Matrix4 transform = selectables[index].getTransformTo(null); | ||
final Rect globalRect = MatrixUtils.transformRect(transform, localRect); | ||
if (globalRect.contains(event.globalPosition)) { | ||
final SelectionGeometry existingGeometry = selectables[index].value; | ||
dispatchSelectionEventToChild(selectables[index], event); | ||
lastSelectionResult = dispatchSelectionEventToChild(selectables[index], event); | ||
if (lastSelectionResult == SelectionResult.previous || lastSelectionResult == SelectionResult.next) { |
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 seems weird, it should stop as soon as dispatchSelectionEventToChild does not return next.
imagine this case
in sequential order
selectable1 -> next (continue)
selectable2 -> next (continue)
selectable3 -> prev (should stop)
this meant the selectword land in between selectable 2 and 3, one of them should be set to the currentSelectionStartIndex and currentSelectionEndIndex, I don't think it matter which one.
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.
That makes sense, but is this assuming that the selectables are in order of how the text is visually laid out?
Trying something like the following code, and the first selectable in the list is Hello world this is some long text that goes\non two lines
. The second selectable is Word
, and the third selectable is Word Word2
.
If I change it to stop as soon as dispatchSelectionEventToChild
does not return next
then I get the following.
In sequential order this happens.
selectable1 -> prev (should stop)
currentSelectionStartIndex/currentSelectionEndIndex : null
should return SelectionResult.prev
Is this expected? Trying to right click Word
at the beginning of the text fails since we never reach it.
import 'package:flutter/material.dart';
void main() {
runApp(const MainApp());
}
class MainApp extends StatelessWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: Scaffold(
body: Center(
child: SelectionArea(
child: Text.rich(
TextSpan(
children: <InlineSpan>[
TextSpan(text: 'Word'),
WidgetSpan(child: SizedBox.shrink()),
TextSpan(text: ' Hello world this is some long text that goes\non two lines.'),
WidgetSpan(child: SizedBox.shrink()),
TextSpan(text: 'Word'),
TextSpan(text: ' Word2'),
],
),
),
),
),
),
);
}
}
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.
the selectables are in order of how the text is visually laid out?
they should be. I think the issue you encounter was a bug that the function to calculate screen order does not do well when the rect overlaps.
int _compareScreenOrder(Selectable a, Selectable b) { |
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 okay I have tried to fix that, and have updated the handleSelectWord
logic as well.
if (lastSelectionResult == SelectionResult.next) { | ||
return SelectionResult.next; | ||
} else if (lastSelectionResult == SelectionResult.previous) { | ||
return SelectionResult.previous; |
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 doesn't look right. consider these cases, assuming 5 child, skip means the selectword point is not in the rect
selectable 1 -> skipped (continue)
selectable 2 -> next (continue)
selectable 3 -> next (continue)
selectable 4 -> prev (stop here)
currentSelectionStartIndex/currentSelectionEndIndex : 3 or 4
should return SelectionResult.end
selectable 1 -> skipped (continue)
selectable 2 -> prev (stop here)
currentSelectionStartIndex/currentSelectionEndIndex : 2
should return SelectionResult.end
selectable 1 -> prev (stop here)
currentSelectionStartIndex/currentSelectionEndIndex : null
should return SelectionResult.prev
selectable 1 -> skip (continue)
selectable 2 -> next (continue)
selectable 3 -> end (stop here)
currentSelectionStartIndex/currentSelectionEndIndex : 3
should return SelectionResult.end
selectable 1 -> skip (continue)
selectable 2 -> next (continue)
selectable 3 -> skip (stop here)
currentSelectionStartIndex/currentSelectionEndIndex : 2
should return SelectionResult.end
selectable 1 -> skip (continue)
selectable 2 -> skip (continue)
selectable 3 -> skip (continue)
selectable 4 -> next (continue)
selectable 5 -> next (at the end)
currentSelectionStartIndex/currentSelectionEndIndex : null
should return SelectionResult.next
selectable 1 -> skip (continue)
selectable 2 -> skip (continue)
selectable 3 -> skip (continue)
selectable 4 -> skip (continue)
selectable 5 -> skip (at the end)
currentSelectionStartIndex/currentSelectionEndIndex : null
should probably return SelectionResult.next
a73194f
to
faed924
Compare
@@ -1903,9 +1916,12 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai | |||
.forEach((Selectable target) => dispatchSelectionEventToChild(target, const ClearSelectionEvent())); | |||
currentSelectionStartIndex = currentSelectionEndIndex = index; | |||
} | |||
return SelectionResult.end; | |||
return lastSelectionResult; |
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.
It seems this is still not addressed #127478 (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.
You're right, think it should be fixed now. But I'm a bit unsure about this case
5.
selectable 1 -> skip (continue)
selectable 2 -> next (continue)
selectable 3 -> skip (stop here)
currentSelectionStartIndex/currentSelectionEndIndex : 2
should return SelectionResult.end
Why does it stop at the last skip?
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 assume all selectable after 3 are skipped. If not, it is likely a bug in sorting screen order. I am actually not sure whether this should return next or end, but I can't think of a case returning end would be a problem.
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 see, so what this case means is that in a given list of selectables
a next
was reported but it was not followed by a prev
/or end
that would indicate the word is inside of the list of selectables
? Why in this case then do we still have to set the currentSelectionStartIndex/currentSelectionEndIndex : 2, when that might not be where the word is? Thank you for explaining.
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.
After I thought about it more, I think we should return end
to short-circuit the logic. Otherwise the select world
logic would need to go through every selectable in the subtree. to figure out whether the position has hit any selectables.
There are two mechanism in play here: the rect
and selection result
. In theory, we don't need rect
, and we can completely rely on selection result
. The rect
is here so that we can trim the search tree. If we were to return next
in this kind of un-decided case, we might as well remove the rect
check completely.
That also reminding me I was wrong on a previous case. These two case should be the following
selectable 1 -> skip (continue)
selectable 2 -> next (continue)
selectable 3 -> skip (stop here)
currentSelectionStartIndex/currentSelectionEndIndex : 2
should return SelectionResult.end
7
selectable 1 -> skip (continue)
selectable 2 -> skip (continue)
selectable 3 -> skip (continue)
selectable 4 -> skip (continue)
selectable 5 -> skip (at the end)
currentSelectionStartIndex/currentSelectionEndIndex : null
should return SelectionResult.end
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.
basically,
if the first selection return prev, this method return prev
if the [n...length-1] return next, this method return next
else return SelectionResult.end.
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.
Thank you for the very in depth explanation. I understand now.
} | ||
} | ||
if (lastSelectionResult == SelectionResult.next) { | ||
return SelectionResult.next; | ||
} | ||
return SelectionResult.none; |
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 method should never return none
4d8eaaf
to
6940148
Compare
b462cfa
to
0367b09
Compare
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, just one concern about the assert being to strict
} | ||
} | ||
return SelectionResult.none; | ||
assert(lastSelectionResult == 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.
This could be null if every selectable is skipped?
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 should be okay right? We should always return inside of the loop, if we are not that means every selectable was skipped, and in that case lastSelectionResult should always be null. Can't really think of a case where we don't return inside of the loop besides the screen order not being correct.
If lastSelectionResult == prev is inside the loop, it will always return, same for end, and also next followed by a skip, and next as the final selectable.
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 sorry I read the assert wrong. Yes this is correct.
7789882
to
103166f
Compare
… should handle rects inside of rects (flutter/flutter#127478)
… should handle rects inside of rects (flutter/flutter#127478)
… should handle rects inside of rects (flutter/flutter#127478)
Roll Flutter from 0b74153 to 8a5c22e (46 revisions) flutter/flutter@0b74153...8a5c22e 2023-06-07 [email protected] Super tiny MediaQuery doc update (flutter/flutter#127904) 2023-06-07 [email protected] Revert "Make inspector weakly referencing the inspected objects." (flutter/flutter#128436) 2023-06-07 [email protected] Update menu API docs to help developers migrate to m3 (flutter/flutter#128351) 2023-06-07 [email protected] [tools] allow explicitly specifying the JDK to use via a new config setting (flutter/flutter#128264) 2023-06-07 [email protected] Roll Flutter Engine from 6f9df0f988c1 to 59d5444cf06c (3 revisions) (flutter/flutter#128376) 2023-06-07 [email protected] Do not try to load main/default asset image if only higher-res variants exist (flutter/flutter#128143) 2023-06-07 [email protected] Addressed Ambiguity in transform.scale constructor docs (flutter/flutter#128182) 2023-06-07 [email protected] Super tiny fix of dead link (flutter/flutter#128160) 2023-06-07 [email protected] Refactor tests (flutter/flutter#128371) 2023-06-07 [email protected] Make inspector weakly referencing the inspected objects. (flutter/flutter#128095) 2023-06-07 [email protected] Add viewId to PointerEvents (flutter/flutter#128287) 2023-06-07 [email protected] Show error message in release mode when box is not laid out without losing performance (flutter/flutter#126302) 2023-06-06 [email protected] Roll Flutter Engine from ca499463ec2e to 6f9df0f988c1 (1 revision) (flutter/flutter#128363) 2023-06-06 [email protected] Update Draggable YouTube video link (flutter/flutter#128078) 2023-06-06 [email protected] Remove more rounding hacks from TextPainter (flutter/flutter#127826) 2023-06-06 [email protected] Roll Flutter Engine from 4571695f9e76 to ca499463ec2e (1 revision) (flutter/flutter#128356) 2023-06-06 [email protected] [Android] Update plugin and module templates to use Flutter constant for `compileSdkVersion` (flutter/flutter#128073) 2023-06-06 [email protected] handleSelectWord in MultiSelectableSelectionContainerDelegate should handle rects inside of rects (flutter/flutter#127478) 2023-06-06 [email protected] [flutter_tools] never tree shake 0x20 (space) font codepoints on web (flutter/flutter#128302) 2023-06-06 [email protected] Remove `textScaleFactor` dependent logic from `AppBar` (flutter/flutter#128112) 2023-06-06 [email protected] Roll Flutter Engine from b6d37f8f74ad to 4571695f9e76 (6 revisions) (flutter/flutter#128350) 2023-06-06 [email protected] Fix `Null check operator used on a null value` on TextField with contextMenuBuilder (flutter/flutter#128114) 2023-06-06 [email protected] Roll Packages from db4e5c2 to da72219 (10 revisions) (flutter/flutter#128348) 2023-06-06 [email protected] Updating cirrus docker image to ubuntu focal. (flutter/flutter#128291) 2023-06-06 [email protected] Adding example for migrating to navigation drawer (flutter/flutter#128295) 2023-06-06 [email protected] Roll Flutter Engine from 722aad83e5fe to b6d37f8f74ad (2 revisions) (flutter/flutter#128341) 2023-06-06 [email protected] Clean-up viewId casts in flutter_test (flutter/flutter#128256) 2023-06-06 [email protected] Update cherry-pick issue template to more uniform labels. (flutter/flutter#128333) 2023-06-06 [email protected] Use Material3 in the 2D viewport tests (flutter/flutter#128155) 2023-06-06 [email protected] Use a `show` over a `hide` for `test_api` exports (flutter/flutter#128298) 2023-06-06 [email protected] Roll Flutter Engine from aaa7574375a6 to 722aad83e5fe (1 revision) (flutter/flutter#128307) 2023-06-06 [email protected] Migration guide for moving from BottomNavigationBar to NavigationBar (flutter/flutter#128263) 2023-06-06 [email protected] [web] Use 'Uri' instead of 'dart:html' to extract pathname (flutter/flutter#127983) 2023-06-06 [email protected] Roll Flutter Engine from 2b353ae90731 to aaa7574375a6 (4 revisions) (flutter/flutter#128301) 2023-06-05 [email protected] Roll Flutter Engine from 220ece4d9faa to 2b353ae90731 (1 revision) (flutter/flutter#128293) 2023-06-05 49699333+dependabot[bot]@users.noreply.github.com Bump actions/labeler from 4.0.4 to 4.1.0 (flutter/flutter#128290) 2023-06-05 [email protected] Roll Flutter Engine from 7f12e3497428 to 220ece4d9faa (6 revisions) (flutter/flutter#128282) 2023-06-05 [email protected] [framework] attempt non-key solution (flutter/flutter#128273) 2023-06-05 [email protected] [labeler] Fix adding labels when name is directory (flutter/flutter#128243) 2023-06-05 [email protected] Remove scrollbar deprecations isAlwaysShown and hoverThickness (flutter/flutter#127351) 2023-06-05 [email protected] Fix update drag error that made NestedScrollView un-scrollable (flutter/flutter#127718) 2023-06-05 [email protected] Roll Flutter Engine from f9f72388a4da to 7f12e3497428 (4 revisions) (flutter/flutter#128271) 2023-06-05 [email protected] Migrate SemanticsBinding to onSemanticsActionEvent (flutter/flutter#128254) 2023-06-05 [email protected] Roll Flutter Engine from c838a1b05924 to f9f72388a4da (19 revisions) (flutter/flutter#128252) 2023-06-05 [email protected] [framework] force flexible space background to rebuild. (flutter/flutter#128138) 2023-06-05 [email protected] Roll Packages from 75085ed to db4e5c2 (4 revisions) (flutter/flutter#128246) ...
This is a bug introduced by flutter#127478. The "else" logic that changed by this PR is supposed to run even if the last child returns "next". Without the fix, there would be an assert error.
… should handle rects inside of rects (flutter/flutter#127478)
… should handle rects inside of rects (flutter/flutter#127478)
… should handle rects inside of rects (flutter/flutter#127478)
… should handle rects inside of rects (flutter/flutter#127478)
Fixes #127076
Sometimes a
Selectable
s rect may contain anotherSelectable
s rect within it. In the case ofhandleSelectWord
when choosing whichSelectable
to dispatch theSelectionEvent
to, the event would be dispatched to the wrongSelectable
causing an assertion error to be thrown.In the picture above the red outline shows the rect of a two-line piece of text. And the blue rect shows the rect of a piece of text that is on the second line of the two-line piece of text, but has been separated into its own rect for some case, for example when
TextSpan
s are separated by aWidgetSpan
.We should check if the text layout of the selectable that has been dispatched the SelectionEvent contains the word, if not then return
SelectionResult.next
, and continue to look through the list of selectables.Pre-launch Checklist
///
).