1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
2 | // Use of this source code is governed by a BSD-style license that can be |
3 | // found in the LICENSE file. |
4 | |
5 | import 'package:flutter/foundation.dart'; |
6 | import 'package:vector_math/vector_math_64.dart' ; |
7 | |
8 | import 'layer.dart'; |
9 | import 'object.dart'; |
10 | |
11 | /// The result after handling a [SelectionEvent]. |
12 | /// |
13 | /// [SelectionEvent]s are sent from [SelectionRegistrar] to be handled by |
14 | /// [SelectionHandler.dispatchSelectionEvent]. The subclasses of |
15 | /// [SelectionHandler] or [Selectable] must return appropriate |
16 | /// [SelectionResult]s after handling the events. |
17 | /// |
18 | /// This is used by the [SelectionContainer] to determine how a selection |
19 | /// expands across its [Selectable] children. |
20 | enum SelectionResult { |
21 | /// There is nothing left to select forward in this [Selectable], and further |
22 | /// selection should extend to the next [Selectable] in screen order. |
23 | /// |
24 | /// {@template flutter.rendering.selection.SelectionResult.footNote} |
25 | /// This is used after subclasses [SelectionHandler] or [Selectable] handled |
26 | /// [SelectionEdgeUpdateEvent]. |
27 | /// {@endtemplate} |
28 | next, |
29 | /// Selection does not reach this [Selectable] and is located before it in |
30 | /// screen order. |
31 | /// |
32 | /// {@macro flutter.rendering.selection.SelectionResult.footNote} |
33 | previous, |
34 | /// Selection ends in this [Selectable]. |
35 | /// |
36 | /// Part of the [Selectable] may or may not be selected, but there is still |
37 | /// content to select forward or backward. |
38 | /// |
39 | /// {@macro flutter.rendering.selection.SelectionResult.footNote} |
40 | end, |
41 | /// The result can't be determined in this frame. |
42 | /// |
43 | /// This is typically used when the subtree is scrolling to reveal more |
44 | /// content. |
45 | /// |
46 | /// {@macro flutter.rendering.selection.SelectionResult.footNote} |
47 | // See `_SelectableRegionState._triggerSelectionEndEdgeUpdate` for how this |
48 | // result affects the selection. |
49 | pending, |
50 | /// There is no result for the selection event. |
51 | /// |
52 | /// This is used when a selection result is not applicable, e.g. |
53 | /// [SelectAllSelectionEvent], [ClearSelectionEvent], and |
54 | /// [SelectWordSelectionEvent]. |
55 | none, |
56 | } |
57 | |
58 | /// The abstract interface to handle [SelectionEvent]s. |
59 | /// |
60 | /// This interface is extended by [Selectable] and [SelectionContainerDelegate] |
61 | /// and is typically not used directly. |
62 | /// |
63 | /// {@template flutter.rendering.SelectionHandler} |
64 | /// This class returns a [SelectionGeometry] as its [value], and is responsible |
65 | /// to notify its listener when its selection geometry has changed as the result |
66 | /// of receiving selection events. |
67 | /// {@endtemplate} |
68 | abstract class SelectionHandler implements ValueListenable<SelectionGeometry> { |
69 | /// Marks this handler to be responsible for pushing [LeaderLayer]s for the |
70 | /// selection handles. |
71 | /// |
72 | /// This handler is responsible for pushing the leader layers with the |
73 | /// given layer links if they are not null. It is possible that only one layer |
74 | /// is non-null if this handler is only responsible for pushing one layer |
75 | /// link. |
76 | /// |
77 | /// The `startHandle` needs to be placed at the visual location of selection |
78 | /// start, the `endHandle` needs to be placed at the visual location of selection |
79 | /// end. Typically, the visual locations should be the same as |
80 | /// [SelectionGeometry.startSelectionPoint] and |
81 | /// [SelectionGeometry.endSelectionPoint]. |
82 | void pushHandleLayers(LayerLink? startHandle, LayerLink? endHandle); |
83 | |
84 | /// Gets the selected content in this object. |
85 | /// |
86 | /// Return `null` if nothing is selected. |
87 | SelectedContent? getSelectedContent(); |
88 | |
89 | /// Handles the [SelectionEvent] sent to this object. |
90 | /// |
91 | /// The subclasses need to update their selections or delegate the |
92 | /// [SelectionEvent]s to their subtrees. |
93 | /// |
94 | /// The `event`s are subclasses of [SelectionEvent]. Check |
95 | /// [SelectionEvent.type] to determine what kinds of event are dispatched to |
96 | /// this handler and handle them accordingly. |
97 | /// |
98 | /// See also: |
99 | /// * [SelectionEventType], which contains all of the possible types. |
100 | SelectionResult dispatchSelectionEvent(SelectionEvent event); |
101 | } |
102 | |
103 | /// The selected content in a [Selectable] or [SelectionHandler]. |
104 | // TODO(chunhtai): Add more support for rich content. |
105 | // https://github.com/flutter/flutter/issues/104206. |
106 | class SelectedContent { |
107 | /// Creates a selected content object. |
108 | /// |
109 | /// Only supports plain text. |
110 | const SelectedContent({required this.plainText}); |
111 | |
112 | /// The selected content in plain text format. |
113 | final String plainText; |
114 | } |
115 | |
116 | /// A mixin that can be selected by users when under a [SelectionArea] widget. |
117 | /// |
118 | /// This object receives selection events and the [value] must reflect the |
119 | /// current selection in this [Selectable]. The object must also notify its |
120 | /// listener if the [value] ever changes. |
121 | /// |
122 | /// This object is responsible for drawing the selection highlight. |
123 | /// |
124 | /// In order to receive the selection event, the mixer needs to register |
125 | /// itself to [SelectionRegistrar]s. Use |
126 | /// [SelectionContainer.maybeOf] to get the selection registrar, and |
127 | /// mix the [SelectionRegistrant] to subscribe to the [SelectionRegistrar] |
128 | /// automatically. |
129 | /// |
130 | /// This mixin is typically mixed by [RenderObject]s. The [RenderObject.paint] |
131 | /// methods are responsible to push the [LayerLink]s provided to |
132 | /// [pushHandleLayers]. |
133 | /// |
134 | /// {@macro flutter.rendering.SelectionHandler} |
135 | /// |
136 | /// See also: |
137 | /// * [SelectionArea], which provides an overview of selection system. |
138 | mixin Selectable implements SelectionHandler { |
139 | /// {@macro flutter.rendering.RenderObject.getTransformTo} |
140 | Matrix4 getTransformTo(RenderObject? ancestor); |
141 | |
142 | /// The size of this [Selectable]. |
143 | Size get size; |
144 | |
145 | /// A list of [Rect]s that represent the bounding box of this [Selectable] |
146 | /// in local coordinates. |
147 | List<Rect> get boundingBoxes; |
148 | |
149 | /// Disposes resources held by the mixer. |
150 | void dispose(); |
151 | } |
152 | |
153 | /// A mixin to auto-register the mixer to the [registrar]. |
154 | /// |
155 | /// To use this mixin, the mixer needs to set the [registrar] to the |
156 | /// [SelectionRegistrar] it wants to register to. |
157 | /// |
158 | /// This mixin only registers the mixer with the [registrar] if the |
159 | /// [SelectionGeometry.hasContent] returned by the mixer is true. |
160 | mixin SelectionRegistrant on Selectable { |
161 | /// The [SelectionRegistrar] the mixer will be or is registered to. |
162 | /// |
163 | /// This [Selectable] only registers the mixer if the |
164 | /// [SelectionGeometry.hasContent] returned by the [Selectable] is true. |
165 | SelectionRegistrar? get registrar => _registrar; |
166 | SelectionRegistrar? _registrar; |
167 | set registrar(SelectionRegistrar? value) { |
168 | if (value == _registrar) { |
169 | return; |
170 | } |
171 | if (value == null) { |
172 | // When registrar goes from non-null to null; |
173 | removeListener(_updateSelectionRegistrarSubscription); |
174 | } else if (_registrar == null) { |
175 | // When registrar goes from null to non-null; |
176 | addListener(_updateSelectionRegistrarSubscription); |
177 | } |
178 | _removeSelectionRegistrarSubscription(); |
179 | _registrar = value; |
180 | _updateSelectionRegistrarSubscription(); |
181 | } |
182 | |
183 | @override |
184 | void dispose() { |
185 | _removeSelectionRegistrarSubscription(); |
186 | super.dispose(); |
187 | } |
188 | |
189 | bool _subscribedToSelectionRegistrar = false; |
190 | void _updateSelectionRegistrarSubscription() { |
191 | if (_registrar == null) { |
192 | _subscribedToSelectionRegistrar = false; |
193 | return; |
194 | } |
195 | if (_subscribedToSelectionRegistrar && !value.hasContent) { |
196 | _registrar!.remove(this); |
197 | _subscribedToSelectionRegistrar = false; |
198 | } else if (!_subscribedToSelectionRegistrar && value.hasContent) { |
199 | _registrar!.add(this); |
200 | _subscribedToSelectionRegistrar = true; |
201 | } |
202 | } |
203 | |
204 | void _removeSelectionRegistrarSubscription() { |
205 | if (_subscribedToSelectionRegistrar) { |
206 | _registrar!.remove(this); |
207 | _subscribedToSelectionRegistrar = false; |
208 | } |
209 | } |
210 | } |
211 | |
212 | /// A utility class that provides useful methods for handling selection events. |
213 | abstract final class SelectionUtils { |
214 | /// Determines [SelectionResult] purely based on the target rectangle. |
215 | /// |
216 | /// This method returns [SelectionResult.end] if the `point` is inside the |
217 | /// `targetRect`. Returns [SelectionResult.previous] if the `point` is |
218 | /// considered to be lower than `targetRect` in screen order. Returns |
219 | /// [SelectionResult.next] if the point is considered to be higher than |
220 | /// `targetRect` in screen order. |
221 | static SelectionResult getResultBasedOnRect(Rect targetRect, Offset point) { |
222 | if (targetRect.contains(point)) { |
223 | return SelectionResult.end; |
224 | } |
225 | if (point.dy < targetRect.top) { |
226 | return SelectionResult.previous; |
227 | } |
228 | if (point.dy > targetRect.bottom) { |
229 | return SelectionResult.next; |
230 | } |
231 | return point.dx >= targetRect.right |
232 | ? SelectionResult.next |
233 | : SelectionResult.previous; |
234 | } |
235 | |
236 | /// Adjusts the dragging offset based on the target rect. |
237 | /// |
238 | /// This method moves the offsets to be within the target rect in case they are |
239 | /// outside the rect. |
240 | /// |
241 | /// This is used in the case where a drag happens outside of the rectangle |
242 | /// of a [Selectable]. |
243 | /// |
244 | /// The logic works as the following: |
245 | /// ![](https://flutter.github.io/assets-for-api-docs/assets/rendering/adjust_drag_offset.png) |
246 | /// |
247 | /// For points inside the rect: |
248 | /// Their effective locations are unchanged. |
249 | /// |
250 | /// For points in Area 1: |
251 | /// Move them to top-left of the rect if text direction is ltr, or top-right |
252 | /// if rtl. |
253 | /// |
254 | /// For points in Area 2: |
255 | /// Move them to bottom-right of the rect if text direction is ltr, or |
256 | /// bottom-left if rtl. |
257 | static Offset adjustDragOffset(Rect targetRect, Offset point, {TextDirection direction = TextDirection.ltr}) { |
258 | if (targetRect.contains(point)) { |
259 | return point; |
260 | } |
261 | if (point.dy <= targetRect.top || |
262 | point.dy <= targetRect.bottom && point.dx <= targetRect.left) { |
263 | // Area 1 |
264 | return direction == TextDirection.ltr ? targetRect.topLeft : targetRect.topRight; |
265 | } else { |
266 | // Area 2 |
267 | return direction == TextDirection.ltr ? targetRect.bottomRight : targetRect.bottomLeft; |
268 | } |
269 | } |
270 | } |
271 | |
272 | /// The type of a [SelectionEvent]. |
273 | /// |
274 | /// Used by [SelectionEvent.type] to distinguish different types of events. |
275 | enum SelectionEventType { |
276 | /// An event to update the selection start edge. |
277 | /// |
278 | /// Used by [SelectionEdgeUpdateEvent]. |
279 | startEdgeUpdate, |
280 | |
281 | /// An event to update the selection end edge. |
282 | /// |
283 | /// Used by [SelectionEdgeUpdateEvent]. |
284 | endEdgeUpdate, |
285 | |
286 | /// An event to clear the current selection. |
287 | /// |
288 | /// Used by [ClearSelectionEvent]. |
289 | clear, |
290 | |
291 | /// An event to select all the available content. |
292 | /// |
293 | /// Used by [SelectAllSelectionEvent]. |
294 | selectAll, |
295 | |
296 | /// An event to select a word at the location |
297 | /// [SelectWordSelectionEvent.globalPosition]. |
298 | /// |
299 | /// Used by [SelectWordSelectionEvent]. |
300 | selectWord, |
301 | |
302 | /// An event that extends the selection by a specific [TextGranularity]. |
303 | granularlyExtendSelection, |
304 | |
305 | /// An event that extends the selection in a specific direction. |
306 | directionallyExtendSelection, |
307 | } |
308 | |
309 | /// The unit of how selection handles move in text. |
310 | /// |
311 | /// The [GranularlyExtendSelectionEvent] uses this enum to describe how |
312 | /// [Selectable] should extend its selection. |
313 | enum TextGranularity { |
314 | /// Treats each character as an atomic unit when moving the selection handles. |
315 | character, |
316 | |
317 | /// Treats word as an atomic unit when moving the selection handles. |
318 | word, |
319 | |
320 | /// Treats each line break as an atomic unit when moving the selection handles. |
321 | line, |
322 | |
323 | /// Treats the entire document as an atomic unit when moving the selection handles. |
324 | document, |
325 | } |
326 | |
327 | /// An abstract base class for selection events. |
328 | /// |
329 | /// This should not be directly used. To handle a selection event, it should |
330 | /// be downcast to a specific subclass. One can use [type] to look up which |
331 | /// subclasses to downcast to. |
332 | /// |
333 | /// See also: |
334 | /// * [SelectAllSelectionEvent], for events to select all contents. |
335 | /// * [ClearSelectionEvent], for events to clear selections. |
336 | /// * [SelectWordSelectionEvent], for events to select words at the locations. |
337 | /// * [SelectionEdgeUpdateEvent], for events to update selection edges. |
338 | /// * [SelectionEventType], for determining the subclass types. |
339 | abstract class SelectionEvent { |
340 | const SelectionEvent._(this.type); |
341 | |
342 | /// The type of this selection event. |
343 | final SelectionEventType type; |
344 | } |
345 | |
346 | /// Selects all selectable contents. |
347 | /// |
348 | /// This event can be sent as the result of keyboard select-all, i.e. |
349 | /// ctrl + A, or cmd + A in macOS. |
350 | class SelectAllSelectionEvent extends SelectionEvent { |
351 | /// Creates a select all selection event. |
352 | const SelectAllSelectionEvent(): super._(SelectionEventType.selectAll); |
353 | } |
354 | |
355 | /// Clears the selection from the [Selectable] and removes any existing |
356 | /// highlight as if there is no selection at all. |
357 | class ClearSelectionEvent extends SelectionEvent { |
358 | /// Create a clear selection event. |
359 | const ClearSelectionEvent(): super._(SelectionEventType.clear); |
360 | } |
361 | |
362 | /// Selects the whole word at the location. |
363 | /// |
364 | /// This event can be sent as the result of mobile long press selection. |
365 | class SelectWordSelectionEvent extends SelectionEvent { |
366 | /// Creates a select word event at the [globalPosition]. |
367 | const SelectWordSelectionEvent({required this.globalPosition}): super._(SelectionEventType.selectWord); |
368 | |
369 | /// The position in global coordinates to select word at. |
370 | final Offset globalPosition; |
371 | } |
372 | |
373 | /// Updates a selection edge. |
374 | /// |
375 | /// An active selection contains two edges, start and end. Use the [type] to |
376 | /// determine which edge this event applies to. If the [type] is |
377 | /// [SelectionEventType.startEdgeUpdate], the event updates start edge. If the |
378 | /// [type] is [SelectionEventType.endEdgeUpdate], the event updates end edge. |
379 | /// |
380 | /// The [globalPosition] contains the new offset of the edge. |
381 | /// |
382 | /// The [granularity] contains the granularity that the selection edge should move by. |
383 | /// Only [TextGranularity.character] and [TextGranularity.word] are currently supported. |
384 | /// |
385 | /// This event is dispatched when the framework detects [TapDragStartDetails] in |
386 | /// [SelectionArea]'s gesture recognizers for mouse devices, or the selection |
387 | /// handles have been dragged to new locations. |
388 | class SelectionEdgeUpdateEvent extends SelectionEvent { |
389 | /// Creates a selection start edge update event. |
390 | /// |
391 | /// The [globalPosition] contains the location of the selection start edge. |
392 | /// |
393 | /// The [granularity] contains the granularity which the selection edge should move by. |
394 | /// This value defaults to [TextGranularity.character]. |
395 | const SelectionEdgeUpdateEvent.forStart({ |
396 | required this.globalPosition, |
397 | TextGranularity? granularity |
398 | }) : granularity = granularity ?? TextGranularity.character, super._(SelectionEventType.startEdgeUpdate); |
399 | |
400 | /// Creates a selection end edge update event. |
401 | /// |
402 | /// The [globalPosition] contains the new location of the selection end edge. |
403 | /// |
404 | /// The [granularity] contains the granularity which the selection edge should move by. |
405 | /// This value defaults to [TextGranularity.character]. |
406 | const SelectionEdgeUpdateEvent.forEnd({ |
407 | required this.globalPosition, |
408 | TextGranularity? granularity |
409 | }) : granularity = granularity ?? TextGranularity.character, super._(SelectionEventType.endEdgeUpdate); |
410 | |
411 | /// The new location of the selection edge. |
412 | final Offset globalPosition; |
413 | |
414 | /// The granularity for which the selection moves. |
415 | /// |
416 | /// Only [TextGranularity.character] and [TextGranularity.word] are currently supported. |
417 | /// |
418 | /// Defaults to [TextGranularity.character]. |
419 | final TextGranularity granularity; |
420 | } |
421 | |
422 | /// Extends the start or end of the selection by a given [TextGranularity]. |
423 | /// |
424 | /// To handle this event, move the associated selection edge, as dictated by |
425 | /// [isEnd], according to the [granularity]. |
426 | class GranularlyExtendSelectionEvent extends SelectionEvent { |
427 | /// Creates a [GranularlyExtendSelectionEvent]. |
428 | const GranularlyExtendSelectionEvent({ |
429 | required this.forward, |
430 | required this.isEnd, |
431 | required this.granularity, |
432 | }) : super._(SelectionEventType.granularlyExtendSelection); |
433 | |
434 | /// Whether to extend the selection forward. |
435 | final bool forward; |
436 | |
437 | /// Whether this event is updating the end selection edge. |
438 | final bool isEnd; |
439 | |
440 | /// The granularity for which the selection extend. |
441 | final TextGranularity granularity; |
442 | } |
443 | |
444 | /// The direction to extend a selection. |
445 | /// |
446 | /// The [DirectionallyExtendSelectionEvent] uses this enum to describe how |
447 | /// [Selectable] should extend their selection. |
448 | enum SelectionExtendDirection { |
449 | /// Move one edge of the selection vertically to the previous adjacent line. |
450 | /// |
451 | /// For text selection, it should consider both soft and hard linebreak. |
452 | /// |
453 | /// See [DirectionallyExtendSelectionEvent.dx] on how to |
454 | /// calculate the horizontal offset. |
455 | previousLine, |
456 | |
457 | /// Move one edge of the selection vertically to the next adjacent line. |
458 | /// |
459 | /// For text selection, it should consider both soft and hard linebreak. |
460 | /// |
461 | /// See [DirectionallyExtendSelectionEvent.dx] on how to |
462 | /// calculate the horizontal offset. |
463 | nextLine, |
464 | |
465 | /// Move the selection edges forward to a certain horizontal offset in the |
466 | /// same line. |
467 | /// |
468 | /// If there is no on-going selection, the selection must start with the first |
469 | /// line (or equivalence of first line in a non-text selectable) and select |
470 | /// toward the horizontal offset in the same line. |
471 | /// |
472 | /// The selectable that receives [DirectionallyExtendSelectionEvent] with this |
473 | /// enum must return [SelectionResult.end]. |
474 | /// |
475 | /// See [DirectionallyExtendSelectionEvent.dx] on how to |
476 | /// calculate the horizontal offset. |
477 | forward, |
478 | |
479 | /// Move the selection edges backward to a certain horizontal offset in the |
480 | /// same line. |
481 | /// |
482 | /// If there is no on-going selection, the selection must start with the last |
483 | /// line (or equivalence of last line in a non-text selectable) and select |
484 | /// backward the horizontal offset in the same line. |
485 | /// |
486 | /// The selectable that receives [DirectionallyExtendSelectionEvent] with this |
487 | /// enum must return [SelectionResult.end]. |
488 | /// |
489 | /// See [DirectionallyExtendSelectionEvent.dx] on how to |
490 | /// calculate the horizontal offset. |
491 | backward, |
492 | } |
493 | |
494 | /// Extends the current selection with respect to a [direction]. |
495 | /// |
496 | /// To handle this event, move the associated selection edge, as dictated by |
497 | /// [isEnd], according to the [direction]. |
498 | /// |
499 | /// The movements are always based on [dx]. The value is in |
500 | /// global coordinates and is the horizontal offset the selection edge should |
501 | /// move to when moving to across lines. |
502 | class DirectionallyExtendSelectionEvent extends SelectionEvent { |
503 | /// Creates a [DirectionallyExtendSelectionEvent]. |
504 | const DirectionallyExtendSelectionEvent({ |
505 | required this.dx, |
506 | required this.isEnd, |
507 | required this.direction, |
508 | }) : super._(SelectionEventType.directionallyExtendSelection); |
509 | |
510 | /// The horizontal offset the selection should move to. |
511 | /// |
512 | /// The offset is in global coordinates. |
513 | final double dx; |
514 | |
515 | /// Whether this event is updating the end selection edge. |
516 | final bool isEnd; |
517 | |
518 | /// The directional movement of this event. |
519 | /// |
520 | /// See also: |
521 | /// * [SelectionExtendDirection], which explains how to handle each enum. |
522 | final SelectionExtendDirection direction; |
523 | |
524 | /// Makes a copy of this object with its property replaced with the new |
525 | /// values. |
526 | DirectionallyExtendSelectionEvent copyWith({ |
527 | double? dx, |
528 | bool? isEnd, |
529 | SelectionExtendDirection? direction, |
530 | }) { |
531 | return DirectionallyExtendSelectionEvent( |
532 | dx: dx ?? this.dx, |
533 | isEnd: isEnd ?? this.isEnd, |
534 | direction: direction ?? this.direction, |
535 | ); |
536 | } |
537 | } |
538 | |
539 | /// A registrar that keeps track of [Selectable]s in the subtree. |
540 | /// |
541 | /// A [Selectable] is only included in the [SelectableRegion] if they are |
542 | /// registered with a [SelectionRegistrar]. Once a [Selectable] is registered, |
543 | /// it will receive [SelectionEvent]s in |
544 | /// [SelectionHandler.dispatchSelectionEvent]. |
545 | /// |
546 | /// Use [SelectionContainer.maybeOf] to get the immediate [SelectionRegistrar] |
547 | /// in the ancestor chain above the build context. |
548 | /// |
549 | /// See also: |
550 | /// * [SelectableRegion], which provides an overview of the selection system. |
551 | /// * [SelectionRegistrarScope], which hosts the [SelectionRegistrar] for the |
552 | /// subtree. |
553 | /// * [SelectionRegistrant], which auto registers the object with the mixin to |
554 | /// [SelectionRegistrar]. |
555 | abstract class SelectionRegistrar { |
556 | /// Adds the [selectable] into the registrar. |
557 | /// |
558 | /// A [Selectable] must register with the [SelectionRegistrar] in order to |
559 | /// receive selection events. |
560 | void add(Selectable selectable); |
561 | |
562 | /// Removes the [selectable] from the registrar. |
563 | /// |
564 | /// A [Selectable] must unregister itself if it is removed from the rendering |
565 | /// tree. |
566 | void remove(Selectable selectable); |
567 | } |
568 | |
569 | /// The status that indicates whether there is a selection and whether the |
570 | /// selection is collapsed. |
571 | /// |
572 | /// A collapsed selection means the selection starts and ends at the same |
573 | /// location. |
574 | enum SelectionStatus { |
575 | /// The selection is not collapsed. |
576 | /// |
577 | /// For example if `{}` represent the selection edges: |
578 | /// 'ab{cd}', the collapsing status is [uncollapsed]. |
579 | /// '{abcd}', the collapsing status is [uncollapsed]. |
580 | uncollapsed, |
581 | |
582 | /// The selection is collapsed. |
583 | /// |
584 | /// For example if `{}` represent the selection edges: |
585 | /// 'ab{}cd', the collapsing status is [collapsed]. |
586 | /// '{}abcd', the collapsing status is [collapsed]. |
587 | /// 'abcd{}', the collapsing status is [collapsed]. |
588 | collapsed, |
589 | |
590 | /// No selection. |
591 | none, |
592 | } |
593 | |
594 | /// The geometry of the current selection. |
595 | /// |
596 | /// This includes details such as the locations of the selection start and end, |
597 | /// line height, the rects that encompass the selection, etc. This information |
598 | /// is used for drawing selection controls for mobile platforms. |
599 | /// |
600 | /// The positions in geometry are in local coordinates of the [SelectionHandler] |
601 | /// or [Selectable]. |
602 | @immutable |
603 | class SelectionGeometry { |
604 | /// Creates a selection geometry object. |
605 | /// |
606 | /// If any of the [startSelectionPoint] and [endSelectionPoint] is not null, |
607 | /// the [status] must not be [SelectionStatus.none]. |
608 | const SelectionGeometry({ |
609 | this.startSelectionPoint, |
610 | this.endSelectionPoint, |
611 | this.selectionRects = const <Rect>[], |
612 | required this.status, |
613 | required this.hasContent, |
614 | }) : assert((startSelectionPoint == null && endSelectionPoint == null) || status != SelectionStatus.none); |
615 | |
616 | /// The geometry information at the selection start. |
617 | /// |
618 | /// This information is used for drawing mobile selection controls. The |
619 | /// [SelectionPoint.localPosition] of the selection start is typically at the |
620 | /// start of the selection highlight at where the start selection handle |
621 | /// should be drawn. |
622 | /// |
623 | /// The [SelectionPoint.handleType] should be [TextSelectionHandleType.left] |
624 | /// for forward selection or [TextSelectionHandleType.right] for backward |
625 | /// selection in most cases. |
626 | /// |
627 | /// Can be null if the selection start is offstage, for example, when the |
628 | /// selection is outside of the viewport or is kept alive by a scrollable. |
629 | final SelectionPoint? startSelectionPoint; |
630 | |
631 | /// The geometry information at the selection end. |
632 | /// |
633 | /// This information is used for drawing mobile selection controls. The |
634 | /// [SelectionPoint.localPosition] of the selection end is typically at the end |
635 | /// of the selection highlight at where the end selection handle should be |
636 | /// drawn. |
637 | /// |
638 | /// The [SelectionPoint.handleType] should be [TextSelectionHandleType.right] |
639 | /// for forward selection or [TextSelectionHandleType.left] for backward |
640 | /// selection in most cases. |
641 | /// |
642 | /// Can be null if the selection end is offstage, for example, when the |
643 | /// selection is outside of the viewport or is kept alive by a scrollable. |
644 | final SelectionPoint? endSelectionPoint; |
645 | |
646 | /// The status of ongoing selection in the [Selectable] or [SelectionHandler]. |
647 | final SelectionStatus status; |
648 | |
649 | /// The rects in the local coordinates of the containing [Selectable] that |
650 | /// represent the selection if there is any. |
651 | final List<Rect> selectionRects; |
652 | |
653 | /// Whether there is any selectable content in the [Selectable] or |
654 | /// [SelectionHandler]. |
655 | final bool hasContent; |
656 | |
657 | /// Whether there is an ongoing selection. |
658 | bool get hasSelection => status != SelectionStatus.none; |
659 | |
660 | /// Makes a copy of this object with the given values updated. |
661 | SelectionGeometry copyWith({ |
662 | SelectionPoint? startSelectionPoint, |
663 | SelectionPoint? endSelectionPoint, |
664 | List<Rect>? selectionRects, |
665 | SelectionStatus? status, |
666 | bool? hasContent, |
667 | }) { |
668 | return SelectionGeometry( |
669 | startSelectionPoint: startSelectionPoint ?? this.startSelectionPoint, |
670 | endSelectionPoint: endSelectionPoint ?? this.endSelectionPoint, |
671 | selectionRects: selectionRects ?? this.selectionRects, |
672 | status: status ?? this.status, |
673 | hasContent: hasContent ?? this.hasContent, |
674 | ); |
675 | } |
676 | |
677 | @override |
678 | bool operator ==(Object other) { |
679 | if (identical(this, other)) { |
680 | return true; |
681 | } |
682 | if (other.runtimeType != runtimeType) { |
683 | return false; |
684 | } |
685 | return other is SelectionGeometry |
686 | && other.startSelectionPoint == startSelectionPoint |
687 | && other.endSelectionPoint == endSelectionPoint |
688 | && other.selectionRects == selectionRects |
689 | && other.status == status |
690 | && other.hasContent == hasContent; |
691 | } |
692 | |
693 | @override |
694 | int get hashCode { |
695 | return Object.hash( |
696 | startSelectionPoint, |
697 | endSelectionPoint, |
698 | selectionRects, |
699 | status, |
700 | hasContent, |
701 | ); |
702 | } |
703 | } |
704 | |
705 | /// The geometry information of a selection point. |
706 | @immutable |
707 | class SelectionPoint with Diagnosticable { |
708 | /// Creates a selection point object. |
709 | const SelectionPoint({ |
710 | required this.localPosition, |
711 | required this.lineHeight, |
712 | required this.handleType, |
713 | }); |
714 | |
715 | /// The position of the selection point in the local coordinates of the |
716 | /// containing [Selectable]. |
717 | final Offset localPosition; |
718 | |
719 | /// The line height at the selection point. |
720 | final double lineHeight; |
721 | |
722 | /// The selection handle type that should be used at the selection point. |
723 | /// |
724 | /// This is used for building the mobile selection handle. |
725 | final TextSelectionHandleType handleType; |
726 | |
727 | @override |
728 | bool operator ==(Object other) { |
729 | if (identical(this, other)) { |
730 | return true; |
731 | } |
732 | if (other.runtimeType != runtimeType) { |
733 | return false; |
734 | } |
735 | return other is SelectionPoint |
736 | && other.localPosition == localPosition |
737 | && other.lineHeight == lineHeight |
738 | && other.handleType == handleType; |
739 | } |
740 | |
741 | @override |
742 | int get hashCode { |
743 | return Object.hash( |
744 | localPosition, |
745 | lineHeight, |
746 | handleType, |
747 | ); |
748 | } |
749 | |
750 | @override |
751 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
752 | super.debugFillProperties(properties); |
753 | properties.add(DiagnosticsProperty<Offset>('localPosition' , localPosition)); |
754 | properties.add(DoubleProperty('lineHeight' , lineHeight)); |
755 | properties.add(EnumProperty<TextSelectionHandleType>('handleType' , handleType)); |
756 | } |
757 | } |
758 | |
759 | /// The type of selection handle to be displayed. |
760 | /// |
761 | /// With mixed-direction text, both handles may be the same type. Examples: |
762 | /// |
763 | /// * LTR text: 'the <quick brown> fox': |
764 | /// |
765 | /// The '<' is drawn with the [left] type, the '>' with the [right] |
766 | /// |
767 | /// * RTL text: 'XOF <NWORB KCIUQ> EHT': |
768 | /// |
769 | /// Same as above. |
770 | /// |
771 | /// * mixed text: '<the NWOR<B KCIUQ fox' |
772 | /// |
773 | /// Here 'the QUICK B' is selected, but 'QUICK BROWN' is RTL. Both are drawn |
774 | /// with the [left] type. |
775 | /// |
776 | /// See also: |
777 | /// |
778 | /// * [TextDirection], which discusses left-to-right and right-to-left text in |
779 | /// more detail. |
780 | enum TextSelectionHandleType { |
781 | /// The selection handle is to the left of the selection end point. |
782 | left, |
783 | |
784 | /// The selection handle is to the right of the selection end point. |
785 | right, |
786 | |
787 | /// The start and end of the selection are co-incident at this point. |
788 | collapsed, |
789 | } |
790 | |