@@ -351,7 +351,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
351351 final LayerLink _startHandleLayerLink = LayerLink ();
352352 final LayerLink _endHandleLayerLink = LayerLink ();
353353 final LayerLink _toolbarLayerLink = LayerLink ();
354- final _SelectableRegionContainerDelegate _selectionDelegate = _SelectableRegionContainerDelegate ();
354+ final StaticSelectionContainerDelegate _selectionDelegate = StaticSelectionContainerDelegate ();
355355 // there should only ever be one selectable, which is the SelectionContainer.
356356 Selectable ? _selectable;
357357
@@ -1823,129 +1823,233 @@ class _DirectionallyExtendCaretSelectionAction<T extends DirectionalCaretMovemen
18231823 }
18241824}
18251825
1826- class _SelectableRegionContainerDelegate extends MultiSelectableSelectionContainerDelegate {
1826+ /// A delegate that manages updating multiple [Selectable] children where the
1827+ /// [Selectable] s do not change or move around frequently.
1828+ ///
1829+ /// This delegate keeps track of the [Selectable] s that received start or end
1830+ /// [SelectionEvent] s and the global locations of those events to accurately
1831+ /// synthesize [SelectionEvent] s for children [Selectable] s when needed.
1832+ ///
1833+ /// When a new [SelectionEdgeUpdateEvent] is dispatched to a [Selectable] , this
1834+ /// delegate checks whether the [Selectable] has already received a selection
1835+ /// update for each edge that currently exists, and synthesizes an event for the
1836+ /// edges that have not yet received an update. This synthesized event is dispatched
1837+ /// before dispatching the new event.
1838+ ///
1839+ /// For example, if we have an existing start edge for this delegate and a [Selectable]
1840+ /// child receives an end [SelectionEdgeUpdateEvent] and the child hasn't received a start
1841+ /// [SelectionEdgeUpdateEvent] , we synthesize a start [SelectionEdgeUpdateEvent] for the
1842+ /// child [Selectable] and dispatch it before dispatching the original end [SelectionEdgeUpdateEvent] .
1843+ ///
1844+ /// See also:
1845+ ///
1846+ /// * [MultiSelectableSelectionContainerDelegate] , for the class that provides
1847+ /// the main implementation details of this [SelectionContainerDelegate] .
1848+ class StaticSelectionContainerDelegate extends MultiSelectableSelectionContainerDelegate {
1849+ /// The set of [Selectable] s that have received start events.
18271850 final Set <Selectable > _hasReceivedStartEvent = < Selectable > {};
1851+
1852+ /// The set of [Selectable] s that have received end events.
18281853 final Set <Selectable > _hasReceivedEndEvent = < Selectable > {};
18291854
1855+ /// The global position of the last selection start edge update.
18301856 Offset ? _lastStartEdgeUpdateGlobalPosition;
1857+
1858+ /// The global position of the last selection end edge update.
18311859 Offset ? _lastEndEdgeUpdateGlobalPosition;
18321860
1833- @override
1834- void remove (Selectable selectable) {
1835- _hasReceivedStartEvent.remove (selectable);
1836- _hasReceivedEndEvent.remove (selectable);
1837- super .remove (selectable);
1861+ /// Tracks whether a selection edge update event for a given [Selectable] was received.
1862+ ///
1863+ /// When `forEnd` is true, the [Selectable] will be registered as having received
1864+ /// an end event. When false, the [Selectable] is registered as having received
1865+ /// a start event.
1866+ ///
1867+ /// When `forEnd` is null, the [Selectable] will be registered as having received both
1868+ /// start and end events.
1869+ ///
1870+ /// Call this method when a [SelectionEvent] is dispatched to a child selectable managed
1871+ /// by this delegate.
1872+ ///
1873+ /// Subclasses should call [clearInternalSelectionStateForSelectable] to clean up any state
1874+ /// added by this method, for example when removing a [Selectable] from this delegate.
1875+ @protected
1876+ void didReceiveSelectionEventFor ({required Selectable selectable, bool ? forEnd}) {
1877+ switch (forEnd) {
1878+ case true :
1879+ _hasReceivedEndEvent.add (selectable);
1880+ case false :
1881+ _hasReceivedStartEvent.add (selectable);
1882+ case null :
1883+ _hasReceivedStartEvent.add (selectable);
1884+ _hasReceivedEndEvent.add (selectable);
1885+ }
18381886 }
18391887
1840- void _updateLastEdgeEventsFromGeometries () {
1888+ /// Updates the internal selection state after a [SelectionEvent] that
1889+ /// selects a boundary such as: [SelectWordSelectionEvent] ,
1890+ /// [SelectParagraphSelectionEvent] , and [SelectAllSelectionEvent] .
1891+ ///
1892+ /// Call this method after determining the new selection as a result of
1893+ /// a [SelectionEvent] that selects a boundary. The [currentSelectionStartIndex]
1894+ /// and [currentSelectionEndIndex] should be set to valid values at the time
1895+ /// this method is called.
1896+ ///
1897+ /// Subclasses should call [clearInternalSelectionStateForSelectable] to clean up any state
1898+ /// added by this method, for example when removing a [Selectable] from this delegate.
1899+ @protected
1900+ void didReceiveSelectionBoundaryEvents () {
1901+ if (currentSelectionStartIndex == - 1 || currentSelectionEndIndex == - 1 ) {
1902+ return ;
1903+ }
1904+ final int start = min (currentSelectionStartIndex, currentSelectionEndIndex);
1905+ final int end = max (currentSelectionStartIndex, currentSelectionEndIndex);
1906+ for (int index = start; index <= end; index += 1 ) {
1907+ didReceiveSelectionEventFor (selectable: selectables[index]);
1908+ }
1909+ _updateLastSelectionEdgeLocationsFromGeometries ();
1910+ }
1911+
1912+ /// Updates the last selection edge location of the edge specified by `forEnd`
1913+ /// to the provided `globalSelectionEdgeLocation` .
1914+ @protected
1915+ void updateLastSelectionEdgeLocation ({required Offset globalSelectionEdgeLocation, required bool forEnd}) {
1916+ if (forEnd) {
1917+ _lastEndEdgeUpdateGlobalPosition = globalSelectionEdgeLocation;
1918+ } else {
1919+ _lastStartEdgeUpdateGlobalPosition = globalSelectionEdgeLocation;
1920+ }
1921+ }
1922+
1923+ /// Updates the last selection edge locations of both start and end selection
1924+ /// edges based on their [SelectionGeometry] .
1925+ void _updateLastSelectionEdgeLocationsFromGeometries () {
18411926 if (currentSelectionStartIndex != - 1 && selectables[currentSelectionStartIndex].value.hasSelection) {
18421927 final Selectable start = selectables[currentSelectionStartIndex];
18431928 final Offset localStartEdge = start.value.startSelectionPoint! .localPosition +
18441929 Offset (0 , - start.value.startSelectionPoint! .lineHeight / 2 );
1845- _lastStartEdgeUpdateGlobalPosition = MatrixUtils .transformPoint (start.getTransformTo (null ), localStartEdge);
1930+ updateLastSelectionEdgeLocation (
1931+ globalSelectionEdgeLocation: MatrixUtils .transformPoint (start.getTransformTo (null ), localStartEdge),
1932+ forEnd: false ,
1933+ );
18461934 }
18471935 if (currentSelectionEndIndex != - 1 && selectables[currentSelectionEndIndex].value.hasSelection) {
18481936 final Selectable end = selectables[currentSelectionEndIndex];
18491937 final Offset localEndEdge = end.value.endSelectionPoint! .localPosition +
18501938 Offset (0 , - end.value.endSelectionPoint! .lineHeight / 2 );
1851- _lastEndEdgeUpdateGlobalPosition = MatrixUtils .transformPoint (end.getTransformTo (null ), localEndEdge);
1939+ updateLastSelectionEdgeLocation (
1940+ globalSelectionEdgeLocation: MatrixUtils .transformPoint (end.getTransformTo (null ), localEndEdge),
1941+ forEnd: true ,
1942+ );
18521943 }
18531944 }
18541945
1946+ /// Clears the internal selection state.
1947+ ///
1948+ /// This indicates that no [Selectable] child under this delegate
1949+ /// has received start or end events, and resets any tracked global
1950+ /// locations for start and end [SelectionEdgeUpdateEvent] s.
1951+ @protected
1952+ void clearInternalSelectionState () {
1953+ selectables.forEach (clearInternalSelectionStateForSelectable);
1954+ _lastStartEdgeUpdateGlobalPosition = null ;
1955+ _lastEndEdgeUpdateGlobalPosition = null ;
1956+ }
1957+
1958+ /// Clears the internal selection state for a given [Selectable] .
1959+ ///
1960+ /// This indicates that the given `selectable` has neither received a
1961+ /// start or end [SelectionEdgeUpdateEvent] s.
1962+ ///
1963+ /// Subclasses should call this method to clean up state added in
1964+ /// [didReceiveSelectionEventFor] and [didReceiveSelectionBoundaryEvents] .
1965+ @protected
1966+ void clearInternalSelectionStateForSelectable (Selectable selectable) {
1967+ _hasReceivedStartEvent.remove (selectable);
1968+ _hasReceivedEndEvent.remove (selectable);
1969+ }
1970+
1971+ @override
1972+ void remove (Selectable selectable) {
1973+ clearInternalSelectionStateForSelectable (selectable);
1974+ super .remove (selectable);
1975+ }
1976+
18551977 @override
18561978 SelectionResult handleSelectAll (SelectAllSelectionEvent event) {
18571979 final SelectionResult result = super .handleSelectAll (event);
1858- for (final Selectable selectable in selectables) {
1859- _hasReceivedStartEvent.add (selectable);
1860- _hasReceivedEndEvent.add (selectable);
1861- }
1862- // Synthesize last update event so the edge updates continue to work.
1863- _updateLastEdgeEventsFromGeometries ();
1980+ didReceiveSelectionBoundaryEvents ();
18641981 return result;
18651982 }
18661983
1867- /// Selects a word in a [Selectable] at the location
1868- /// [SelectWordSelectionEvent.globalPosition] .
18691984 @override
18701985 SelectionResult handleSelectWord (SelectWordSelectionEvent event) {
18711986 final SelectionResult result = super .handleSelectWord (event);
1872- if (currentSelectionStartIndex != - 1 ) {
1873- _hasReceivedStartEvent.add (selectables[currentSelectionStartIndex]);
1874- }
1875- if (currentSelectionEndIndex != - 1 ) {
1876- _hasReceivedEndEvent.add (selectables[currentSelectionEndIndex]);
1877- }
1878- _updateLastEdgeEventsFromGeometries ();
1987+ didReceiveSelectionBoundaryEvents ();
18791988 return result;
18801989 }
18811990
1882- /// Selects a paragraph in a [Selectable] at the location
1883- /// [SelectParagraphSelectionEvent.globalPosition] .
18841991 @override
18851992 SelectionResult handleSelectParagraph (SelectParagraphSelectionEvent event) {
18861993 final SelectionResult result = super .handleSelectParagraph (event);
1887- if (currentSelectionStartIndex != - 1 ) {
1888- _hasReceivedStartEvent.add (selectables[currentSelectionStartIndex]);
1889- }
1890- if (currentSelectionEndIndex != - 1 ) {
1891- _hasReceivedEndEvent.add (selectables[currentSelectionEndIndex]);
1892- }
1893- _updateLastEdgeEventsFromGeometries ();
1994+ didReceiveSelectionBoundaryEvents ();
18941995 return result;
18951996 }
18961997
18971998 @override
18981999 SelectionResult handleClearSelection (ClearSelectionEvent event) {
18992000 final SelectionResult result = super .handleClearSelection (event);
1900- _hasReceivedStartEvent.clear ();
1901- _hasReceivedEndEvent.clear ();
1902- _lastStartEdgeUpdateGlobalPosition = null ;
1903- _lastEndEdgeUpdateGlobalPosition = null ;
2001+ clearInternalSelectionState ();
19042002 return result;
19052003 }
19062004
19072005 @override
19082006 SelectionResult handleSelectionEdgeUpdate (SelectionEdgeUpdateEvent event) {
1909- if (event.type == SelectionEventType .endEdgeUpdate) {
1910- _lastEndEdgeUpdateGlobalPosition = event.globalPosition;
1911- } else {
1912- _lastStartEdgeUpdateGlobalPosition = event.globalPosition;
1913- }
2007+ updateLastSelectionEdgeLocation (
2008+ globalSelectionEdgeLocation: event.globalPosition,
2009+ forEnd: event.type == SelectionEventType .endEdgeUpdate,
2010+ );
19142011 return super .handleSelectionEdgeUpdate (event);
19152012 }
19162013
19172014 @override
19182015 void dispose () {
1919- _hasReceivedStartEvent.clear ();
1920- _hasReceivedEndEvent.clear ();
2016+ clearInternalSelectionState ();
19212017 super .dispose ();
19222018 }
19232019
19242020 @override
19252021 SelectionResult dispatchSelectionEventToChild (Selectable selectable, SelectionEvent event) {
19262022 switch (event.type) {
19272023 case SelectionEventType .startEdgeUpdate:
1928- _hasReceivedStartEvent. add (selectable);
2024+ didReceiveSelectionEventFor (selectable: selectable, forEnd : false );
19292025 ensureChildUpdated (selectable);
19302026 case SelectionEventType .endEdgeUpdate:
1931- _hasReceivedEndEvent. add (selectable);
2027+ didReceiveSelectionEventFor (selectable: selectable, forEnd : true );
19322028 ensureChildUpdated (selectable);
19332029 case SelectionEventType .clear:
1934- _hasReceivedStartEvent.remove (selectable);
1935- _hasReceivedEndEvent.remove (selectable);
2030+ clearInternalSelectionStateForSelectable (selectable);
19362031 case SelectionEventType .selectAll:
19372032 case SelectionEventType .selectWord:
19382033 case SelectionEventType .selectParagraph:
19392034 break ;
19402035 case SelectionEventType .granularlyExtendSelection:
19412036 case SelectionEventType .directionallyExtendSelection:
1942- _hasReceivedStartEvent.add (selectable);
1943- _hasReceivedEndEvent.add (selectable);
2037+ didReceiveSelectionEventFor (selectable: selectable);
19442038 ensureChildUpdated (selectable);
19452039 }
19462040 return super .dispatchSelectionEventToChild (selectable, event);
19472041 }
19482042
2043+ /// Ensures the `selectable` child has received the most up to date selection events.
2044+ ///
2045+ /// This method is called when:
2046+ /// 1. A new [Selectable] is added to the delegate, and its screen location
2047+ /// falls into the previous selection.
2048+ /// 2. Before a [SelectionEvent] of type
2049+ /// [SelectionEventType.startEdgeUpdate] , [SelectionEventType.endEdgeUpdate] ,
2050+ /// [SelectionEventType.granularlyExtendSelection] , or
2051+ /// [SelectionEventType.directionallyExtendSelection] is dispatched
2052+ /// to a [Selectable] child.
19492053 @override
19502054 void ensureChildUpdated (Selectable selectable) {
19512055 if (_lastEndEdgeUpdateGlobalPosition != null && _hasReceivedEndEvent.add (selectable)) {
0 commit comments