-
Notifications
You must be signed in to change notification settings - Fork 29.8k
Add SelectionListener and SelectedContentRange #148998
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
Add SelectionListener and SelectedContentRange #148998
Conversation
b5f7d01 to
f16372f
Compare
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.
Here's my high level review of the approach here.
Just wondering, would it be possible to use a more declarative pattern instead of the imperative SelectedContentController and SelectionController? Like updating values and calling setState instead of calling imperative methods on the controllers.
I also generally worry about multiple sources of truth here, if there is anything that the controllers are duplicating. I mentioned this a little bit in a comment.
|
|
||
| class _MyHomePageState extends State<MyHomePage> { | ||
| static const String _aboutSteve = | ||
| 'Steve is a dog whose personality is as unique as his name. Not your average canine, ' |
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.
Did you get this riveting story from Gemini or something? 🐶
| afterSelection = _trimPlaceholders(afterSelection); | ||
| final List<InlineSpan> concreteSpans = <InlineSpan>[]; | ||
| final TextSpan beforeSpan = TextSpan(text: beforeSelection); | ||
| final TextSpan selectionSpan = TextSpan(text: collectedSelection, style: const TextStyle(color: Colors.red)); |
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 know this is just an example, but does this have the possibility of accumulating deeply nested TextSpans if the user keeps emphasizing the same text? One more reason it would be nice to have annoted strings @LongCatIsLooong .
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 it depends on how the user chooses to implement this type of feature. I chose to work with the plain text in this example, so not a lot of nesting happens.
| /// | ||
| /// A user can set the value on this controller to modify the content under | ||
| /// a selection created by [SelectionArea] or [SelectableRegion]. | ||
| abstract class SelectedContentController<T extends Object> extends ValueNotifier<T> { |
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 the type parameter be an InlineSpan or can it really be anything?
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 made it generic here in case we plan to add support for a different type of format down the road. For example, if Text widgets move from InlineSpan to some new type of annotated string, or if someone has a Selectable state that they only track as a String.
| /// | ||
| /// Children of a given controller enable more granular modification of the | ||
| /// selection. | ||
| List<SelectedContentController<Object>> children = <SelectedContentController<Object>>[]; |
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.
How exactly do the children relate to the value span and its children? Is children supposed to contain all SelectedContentControllers of all other spans in value's subtree?
(Assuming I'm understanding this correctly) I wonder if we can do this in a way that maintains more of a single source of truth.
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.
Correct, the children contain all the SelectedContentControllers that are within the values subtree. For example selecting from "some" to "Bullet 3" in the Text widget below would result in a SelectedContentController with 3 children controllers representing the individual bullet Text widgets,
Text.rich(
TextSpan(
text: 'This is some bulleted list:\n',
children: <InlineSpan>[
WidgetSpan(
child: Column(
children: <Widget>[
for (int i = 1; i <= 7; i += 1)
Padding(
padding: const EdgeInsets.only(left: 20.0),
child: Text('• Bullet $i'),
)
],
),
),
],
),
),I think if we had no children controllers, it would become difficult to edit something like the bulleted text widget in the example, as a user would have to dig into the contents of a WidgetSpan manually using (WidgetSpan.child as Column).children[index] and we would still to communicate the selection ranges for each of the bullets. Open to ideas 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.
Ok I see... My main concern is the controllers getting out of sync with the spans. Can we use immutability somehow to prevent them from getting out of sync?
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 have made the SelectedContentController.children immutable as well now.
| _TextSpanContentController(super.value); | ||
|
|
||
| @override | ||
| TextSpan buildContents() { |
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 is buildContents supposed to be used for? Maybe include an example that overrides it?
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 removed it, I think I was imagining something similar to TextEditingController.buildTextSpan when I first wrote this but it's not needed.
| // Question: Should we add the root text controller, if only the children | ||
| // are selected? For example, say we have the following: | ||
| // Text.rich( | ||
| // TextSpan( | ||
| // text: 'This is some bulleted list:\n', | ||
| // children: <InlineSpan>[ | ||
| // WidgetSpan( | ||
| // child: Column( | ||
| // children: <Widget>[ | ||
| // for (int i = 1; i <= 7; i += 1) | ||
| // Padding( | ||
| // padding: const EdgeInsets.only(left: 20.0), | ||
| // child: Text('• Bullet $i'), | ||
| // ) | ||
| // ], | ||
| // ), | ||
| // ), | ||
| // ], | ||
| // ), | ||
| // ), | ||
| // and we select only text inside the widgetspan, should we return the | ||
| // root text controller in this case, as well as the children controllers | ||
| // that are selected? Or should we only return the selected children controllers. | ||
| // The former might be more confusing to the user? |
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 if a user wants to change something in the part of the text that is not selected? If we want to support that use case then I think we'd have to return the root controller.
chunhtai
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 only took a quick glance, but I am a bit concern about how you can have mismatched SelectedContentController.value and what is declared in the widget tree. Something I am worry is that if somehow a rebuild happen after SelectedContentController.value is changed, the modification will be lost? Why not just have customer update their textspan that they passed into the text widget in the callback and make the SelectedContentController immutable?
| /// | ||
| /// Utilize this controller to programatically clear the content of a | ||
| /// [SelectionArea] or [SelectableRegion], or select all the contents under it. | ||
| class SelectionController with ChangeNotifier { |
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 should probably separate this out to its own pr
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 still the plan?
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 talking with @chunhtai, I think for now it might be less intrusive to expose SelectableRegionState and make its method _clearSelection public. This way we don't tie ourselves to any specific method in this PR.
| return text.replaceAll(String.fromCharCode(PlaceholderSpan.placeholderCodeUnit), ''); | ||
| } | ||
|
|
||
| void _emphasizeText(List<SelectedContentController<Object>>? controllers) { |
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 seems quite complex, I hope we can have a more generic api to modify textspan. In android, you can do something like SpannableString.setSpan(start, end, certainAttribute).
55c9340 to
4b1f060
Compare
|
This is ready for another look. The main changes are that edit: I plan to separate |
|
This is ready for another look. The main changes are that |
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.
Left a few comments about the approach at a high level. I do like the declarative SelectedContentController pattern now.
Why do you need to have selectableId instead of its index in a list?
|
|
||
| /// A list of [SelectedContentController]s that represent the selection, and | ||
| /// can be used to modify the contents of the selection. | ||
| final List<SelectedContentController<Object>>? controllers; |
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 guess the SelectedContentControllers here are only those covered by selection, so there will never be a controller that isn't selected 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.
Yeah you will never get a controller that doesn't include a selection.
| /// | ||
| /// Children of a given controller enable more granular modification of the | ||
| /// selection. | ||
| List<SelectedContentController<Object>> children = <SelectedContentController<Object>>[]; |
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 I see... My main concern is the controllers getting out of sync with the spans. Can we use immutability somehow to prevent them from getting out of sync?
cadfb02 to
c2f5bc9
Compare
| /// | ||
| /// A user can set the value on this controller to modify the content under | ||
| /// a selection created by [SelectionArea] or [SelectableRegion]. | ||
| abstract class SelectedContentController<T extends Object> { |
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.
cc @LongCatIsLooong, this is the relevant code that we were discussing in the text input sync. A list of SelectedContentControllers is provided in SelectionArea.onSelectionChanged representing the active selection. The discussion was regarding whether this should be generic or specifically TextSpan since at the moment the frameworks only selectable widget is Text.
3cd2551 to
f7f695d
Compare
|
Friendly bump for review, changes today mainly include refactoring where the |
4eb2ef2 to
44f56c0
Compare
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 have a bunch of questions about SelectedContent and SelectedContentRange. Sorry for all the nitpicking here. I'm starting to understand this PR more each time I review it so I'm going back to some basics a bit. I think the main thing to do here is just make sure the docs explain the sort of things that I'm asking about.
| } | ||
| expect(find.textContaining('This is some text in a text widget.'), findsOneWidget); | ||
| expect(find.textContaining(' This is some more text in the same text widget.'), findsOneWidget); | ||
| expect(find.textContaining('This is some text in another text widget.'), findsOneWidget); |
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 it's worth it to also test selecting and turning some text red, just because it covers so much of this PR's code in a way that's very realistic. Same for the other example 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.
+1 to this and same for the selection_area.2_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.
TODO: still need to write these tests.
| /// The content that contains the selection. | ||
| final T content; | ||
|
|
||
| /// The start of the selection relative to the [content]. |
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.
"relative to the [content]." => "relative to the start of the [content]."
Is that more accurate? Same for endOffset 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.
(This doesn't seem to have been updated?)
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 talked to @Renzo-Olivares offline about this but yeah I still think this change is important if I'm understanding correctly. I think it helps me be clear on how all of this works. The content could be a smaller part of a larger span tree, so this helps make it clear we're not talking about an offset after the end of the content or something.
Consider:
Say SpanA contains "0" and SpanB, and SpanB contains the rest of the text. If content is SpanB, then startOffset is 8 and endOffset is 9, not 9 and 10, because SpanB doesn't include the first character.
This wording makes it clearer to me that's what we're talking about.
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 explanation! That is clearer.
| /// A list of [SelectedContentRange]s that represent the content under | ||
| /// the active 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.
Are these ranges supposed to cover the entirety of the content that this SelectedContent covers? In my example in my previous comment, that means they should cover "world" but not "hello ", if my assumptions are correct there.
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 SelectedContentRange always includes all the content, and provides a start/end offset relative to the content, so in your example you'll always be given "hello world" and the start: 6/ end: 11.
| class _TextSpanContentRange extends SelectedContentRange<TextSpan> { | ||
| _TextSpanContentRange({ | ||
| this.start = -1, | ||
| this.end = -1, | ||
| super.selectableId, | ||
| required super.content, | ||
| super.children, | ||
| }); | ||
|
|
||
| @override | ||
| int get startOffset => start; | ||
| final int start; | ||
|
|
||
| @override | ||
| int get endOffset => end; | ||
| final int 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.
I wonder if you could effectively get the functionality of _TextSpanContentRange with a type parameter instead of a subclass? Since the only interesting thing here is really the TextSpan type.
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 have made SelectedContentRange non-abstract so we can get rid of these sublasses.
|
I also agree repurposing key is not a good idea. Can we force developer to provide the data model, ie textspan, that forms the selection subtree under the SelectionListener? It seems to me things starts to get weird if they are using SelectionListener without knowing the textspans in the subtree, and thus we try to use key to disambiguate them. However, should that be a valid use case? If they have a subtree look like this. Instead of wrapping a SelectionListener over entire column and try to disambiguate away the UnKnownSelectable, should they just do this instead I don't really know if this can solve your use case, but WDYT? |
|
I don't have a strong opinion on using I do think we should consider including some identifier so the user does not have to use one particular method like multiple One issue I ran into when removing selectableId and updating the color selection red example is that it becomes difficult to know when to clear the selection. Let’s say the selection is spread across a column of 3 text widgets, if we clear the selection in the first text widgets listener then the operations we do in text widget 2 and 3 will not work since the selection will be cleared. This is because text 1 will receive the finalized selection first, do its logic and then clear the selection, not giving text 2 and text 3 a chance to do their logic. The onSelectionChanged from text 1 would need to know if text 2 and text 3 have a selection so it knows not to clear it. This can likely be done by saving the selectionDetails from text 2 and text 3. To me this seems like it adds more cognitive load to the user when compared to having one listener for a subtree, where they can clear the selection at the end of the onSelectionChanged callback. I think the issue of when to clear the selection would also come up if we we force the developer to provide the data model like in @chunhtai example, since they still have to track all the selections from each listener. |
…rator for SelectedContentRange, attempt not call onSelectionChanged when selection finalized is received but the selection has not changed. SelectionFinalizedSelectionEvent does not affect geometry so it is difficult to know if the selection changed between selection finalized events, to fix I keep track of the lastFinalizedSelectionDetails
In this example you want to apply the same effect across 3 widgets (color selection red). In this case, you would wrap one selectionListener for all 3 widgets right? Unless you have condition like widget 2 is some unknown stuff but still selectable and you want to disambiguate widget 2 out. Do you really have a use case like this? e.g. i only want to color the selection widget1 and widget 3 but not widget 2. If you want to react to selection in a part of the screen, you have to know what's inside and thus you can just wrap one listener over the the widget subtree. I may be missing some use cases, so I may be wrong here. |
|
Yeah it would be preferable in the case of the color selection red example to wrap the entire subtree with one listener. I was mainly exploring using multiple to see how the usage of the API changed with multiple. In the example of color selection red, let’s say part of the 3rd widget is selected. Then the user will only receive one SelectedContentRange. Is it reasonable to say that since the user is providing the data model to that subtree that they will know what part of their data model the offsets of the SelectedContentRange belong to? This sounds reasonable to me, though I think if we had the example with unselectable widgets interweaved it might get tricky. I think its also tricky because the offsets you will get in I'm not sure in the example above how the user would know where the range came from, and how to update their data model accordingly. |
That's the case we talked about yesterday right? My thought was that in this case you need an API that query the current |
|
Is the pseudocode below kind of what you're pointing at @LongCatIsLooong? I think this works when using multiple listeners, but am not sure how a user would know if the selection is finalized with this approach. Maybe we can have a SelectionListener.onSelectionFinalized callback, but that seems a little tricky since a user would have to know if the selection has been finalized / is ongoing in In the color red example, where it makes more sense to use one listener to wrap the entire subtree, I think we still run into the issue where a user won't know where the selection came from, using the query approach. |
Do you mean if the user is dragging to expand the selection and at the same time the |
|
Yeah they could wait for the next finalized event before showing the context menu. I wasn't sure with the query approach where exactly they would listen to that finalized event, but the user can wrap the subtree they're listening to with another |
|
I added variants of both examples with/without selectableId. Curious to hear any thoughts regarding pros and cons of either approach. IMO having no selectableId adds another layer of management that the user has to worry about since they now have to track each |
| required this.contentLength, | ||
| required this.startOffset, | ||
| required this.endOffset, | ||
| this.children, |
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.
why does this have children? shouldn't the SelectedContentRange in SelectionDetails.ranges just a flatten list?
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 assume this is for widgetspan, but if they care about selection inside the widgetspan, they should provide a selectionId in the widgetspan themselves
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 the reason I had this as a tree was because a flat list felt discontinuous to me. By that I mean a single Text widget could have multiple SelectedContentRanges if there were multiple WidgetSpans that broke it up vs with a tree structure where each Text widget would have a single SelectedContentRange associated with it and the children of that range are any sub-selections within it.
For example, for the text widget below, with a flattened list selecting all the text would return 3 SelectedContentRanges vs with a tree it only returns 1 SelectedContentRange with 1 child range:
const Text.rich(
TextSpan(
text: 'Hello world, ',
children: <InlineSpan>[
WidgetSpan(
child: Text('how are you today?'),
),
TextSpan(
text: 'More text after widgetspan',
),
],
),
)I'm not sure which approach is better? but I think it makes more sense that a Text widget only reports 1 SelectedContentRange since I don't think the user expects a Text widgets selection to be split into multiple segments.
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 think we should reflect selection range structure with text span tree unless we want to couple selectionListener with text widget. Otherwise, it would be confusing that a certain datamodel will produce selectedcontentrange format in a certain way. It feels like magic.
other way is if we find a way to couple datamodel and selection id, maybe we can tag selection id on per textspan basis.
Another thing is whether the widget span should be included. It feels weird and tricky to include that as one selected content range since widget span can be anything.
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 think we should reflect selection range structure with text span tree unless we want to couple selectionListener with text widget. Otherwise, it would be confusing that a certain datamodel will produce selectedcontentrange format in a certain way. It feels like magic.
I was more thinking this reflected a structure where a leaf widget that is selectable might have children selectables. In most cases this children list will be empty, but in the cases where a selectable does have children then a user could look into this list to see more details. A user could always choose not to look into the children list and only look at the root.
Another issue that gets trickier is ids, if we do return multiple SelectedContentRanges for a single selectable widget then a user will have to likely need to map multiple ranges to a single id. Tagging selection id on per textspan basis would solve this but it seems strange to me to do this in cases when there is no widgetspan. For example say a user has a textspan tree, with multiple textspans it seems odd to require them to tag each individual textspan when the underlying selectable tree would only create one selectable for this case.
Another thing is whether the widget span should be included. It feels weird and tricky to include that as one selected content range since widget span can be anything.
Can you explain more on this? If the widgetspan contains a column of N selectable widgets, then there would be N SelectedContentRanges reported.
|
I think adding selectionId is probably ok, though I think we should figure out a way to
As of using multiple Listener vs one. I don't really know, I guess we will only figure out when we have more use case.
For cases of multipleListener couldn't the isFinalized boolean to be include in the onSelectionChange callback? |
|
I also think there are a lot of different approach that target different use case are proposed here. I think which ever solution we choose should be flexible enough to be able to build on top later. For example, the decision may be that we don't necessary selectionId for now, but we should keep in mind we may need it in the future. so the implementation should not write us into a dead 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.
Quick bug report on this example:
- Select an entire bullet line, so like "* Bullet 2".
- Tap to make it red.
The bullet list disappears.
Just want to call this out in case it's a deeper problem with this approach, but otherwise I'm assuming it's just a small logic error in _colorSelectionRed.
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.
Not sure if you've thought about this idea yet or not but wanted to make sure.
| result = MouseRegion( | ||
| cursor: DefaultSelectionStyle.of(context).mouseCursor ?? SystemMouseCursors.text, | ||
| child: _SelectableTextContainer( | ||
| selectableId: key, |
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.
Instead of key, what if you pass this as the selectableId? Then use only a single SelectionListener and:
- Walk the tree until you find where
selectionDetails.ranges.first.selectableId == textWidget. - Color that subtree red given the matching SelectedContentRange. I think you could do that with a static recursive method like
_makeRedbelow, which might help break up the size of your logic in _colorSelectionRed. - If there are still more ranges in
selectionDetails.ranges, continue walking the tree with the new range. - Repeat until you've called
_makeRedon all the ranges. - Return your new tree, which is the old one but with the selected part red.
I'm assuming that selectionDeteails.ranges is ordered in traversal order.
static InlineSpan _makeRed(TextSpan textSpan, SelectedContentRange range) {
assert(textSpan == range.selectableId);
// I leave the rest as an exercise for the reader.
}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.
Benefits, if it works:
- No explicit selectionId needed on Text.
- Only one SelectionListener is needed for your examples.
- No separate data structure needed in your examples (
dataSourceMapetc.).
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.
Talked offline, this would not work unless we held a reference to the widget, which would still require us to keep some map.
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.
Some quick thoughts after we just met to talk about this with @chunhtai and @LongCatIsLooong.
| // Rebuild column contents. | ||
| for (final MapEntry<int, TextSpan> entry in dataSourceMap.entries) { | ||
| newText.add( | ||
| SelectionListener( |
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 is the order that nested SelectionListeners will have their onSelectionChanged callbacks called? Maybe depth first traversal order or something like that? We should document this so it's clear for users.
This is probably less important if we go with the flattened indices approach, because users will probably just use one top-level SelectionListener. But we should still document it.
| onSelectionChanged: (SelectionDetails selectionDetails) { | ||
| selectionDetailsMap[entry.key] = selectionDetails; | ||
| }, | ||
| child: Text.rich( |
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.
Just for my own curiosity, what was the thought process behind making SelectionListener its own widget vs. adding an onSelectionChanged callback to Text or to SelectionArea? Feel free to just give me a link if this was already discussed elsewhere.
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.
Mainly because given that any complex widget tree could be under SelectionArea we decided it was best to let the user decide if they only want to listen to a portion of thats trees selection changes rather than the entire tree.
| Map<Key, TextSpan> dataSourceMap = <Key, TextSpan>{}; | ||
| Map<Key, TextSpan> bulletSourceMap = <Key, TextSpan>{}; | ||
| late final Map<Key, TextSpan> originSourceData; | ||
| late final Map<Key, TextSpan> originBulletSourceData; |
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.
Back to my ranting about trying to get rid of these maps... So assuming that we go with the "flattened" approach, where onSelectionChanged is called with 2 indices into a flat string, I think you could take the following approach to this example:
- The only state you need is a list of ranges representing the parts of the flat string that have been turned red. No need for any of these Maps.
- Somewhere store your original widget tree under the SelectionListener, with nothing red.
- Write a method
static Widget _makeRed(Widget child, Iterable<TextRange> ranges);. It will walk thechildtree and build a new tree with the given ranges made red. Assert ifrangesexceeds the number of characters you find inchild. - In your build method:
return _makeRed(selectionListenerChild, redRanges); - onSelectionChanged:
(TextRange selection) { setState(() { redRanges.add(selection); }); }. Also, you probably would want to merge overlapping ranges.
Would that work?
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.
Talked offline, but I think that's reasonable. I worry though that a user would technically have to walk two trees in this scenarios, first the Widget tree and then the TextSpan tree for the Text widgets. Walking one tree seems simpler to me.
|
@Renzo-Olivares @chunhtai |
|
Closing this, discussion will continue in #154202. |
https://github.com/user-attachments/assets/59225cf7-5506-414e-87da-aa4d3227e7f6 Adds: * `SelectionListener`, allows a user to listen to selection changes under the subtree it wraps given their is an ancestor `SelectionArea` or `SelectableRegion`. These selection changes can be listened to through the `SelectionListenerNotifier` that is provided to a `SelectionListener`. * `SelectionListenerNotifier`, used with `SelectionListener`, allows a user listen to selection changes for the subtree of the `SelectionListener` it was provided to. Provides access to individual selection values through the `SelectionDetails` object `selection`. * `SelectableRegionSelectionStatusScope`, allows the user to listen to when a parent `SelectableRegion` is changing or finalizing the selection. * `SelectedContentRange`, provides information about the selection range under a `SelectionHandler` or `Selectable` through the `getSelection()` method. This includes a start and end offset relative to the `Selectable`s content. * `SelectionHandler.contentLength`, to describe the length of the content contained by a selectable. Original PR & Discussion: #148998 Fixes: #110594 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. --------- Co-authored-by: Renzo Olivares <roliv@google.com>

Adds
SelectionListenerwhich allows a user to listen to selection changes under the subtree it wraps given their is an ancestorSelectionAreaorSelectableRegion. These selection changes are delivered in the form ofSelectedContentRangewhich provides more information about the active selection under aSelectionAreaandSelectableRegionsuch as the content under the selection, and start and end offsets relative to the content. This is accessible through theonSelectionChangedmember ofSelectionListener.Screen.Recording.2024-05-23.at.2.12.04.PM.mov
Pre-launch Checklist
///).