// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:flutter/foundation.dart'; import 'package:vector_math/vector_math_64.dart'; import 'layer.dart'; import 'object.dart'; /// The result after handling a [SelectionEvent]. /// /// [SelectionEvent]s are sent from [SelectionRegistrar] to be handled by /// [SelectionHandler.dispatchSelectionEvent]. The subclasses of /// [SelectionHandler] or [Selectable] must return appropriate /// [SelectionResult]s after handling the events. /// /// This is used by the [SelectionContainer] to determine how a selection /// expands across its [Selectable] children. enum SelectionResult { /// There is nothing left to select forward in this [Selectable], and further /// selection should extend to the next [Selectable] in screen order. /// /// {@template flutter.rendering.selection.SelectionResult.footNote} /// This is used after subclasses [SelectionHandler] or [Selectable] handled /// [SelectionEdgeUpdateEvent]. /// {@endtemplate} next, /// Selection does not reach this [Selectable] and is located before it in /// screen order. /// /// {@macro flutter.rendering.selection.SelectionResult.footNote} previous, /// Selection ends in this [Selectable]. /// /// Part of the [Selectable] may or may not be selected, but there is still /// content to select forward or backward. /// /// {@macro flutter.rendering.selection.SelectionResult.footNote} end, /// The result can't be determined in this frame. /// /// This is typically used when the subtree is scrolling to reveal more /// content. /// /// {@macro flutter.rendering.selection.SelectionResult.footNote} // See `_SelectableRegionState._triggerSelectionEndEdgeUpdate` for how this // result affects the selection. pending, /// There is no result for the selection event. /// /// This is used when a selection result is not applicable, e.g. /// [SelectAllSelectionEvent], [ClearSelectionEvent], and /// [SelectWordSelectionEvent]. none, } /// The abstract interface to handle [SelectionEvent]s. /// /// This interface is extended by [Selectable] and [SelectionContainerDelegate] /// and is typically not use directly. /// /// {@template flutter.rendering.SelectionHandler} /// This class returns a [SelectionGeometry] as its [value], and is responsible /// to notify its listener when its selection geometry has changed as the result /// of receiving selection events. /// {@endtemplate} abstract class SelectionHandler implements ValueListenable<SelectionGeometry> { /// Marks this handler to be responsible for pushing [LeaderLayer]s for the /// selection handles. /// /// This handler is responsible for pushing the leader layers with the /// given layer links if they are not null. It is possible that only one layer /// is non-null if this handler is only responsible for pushing one layer /// link. /// /// The `startHandle` needs to be placed at the visual location of selection /// start, the `endHandle` needs to be placed at the visual location of selection /// end. Typically, the visual locations should be the same as /// [SelectionGeometry.startSelectionPoint] and /// [SelectionGeometry.endSelectionPoint]. void pushHandleLayers(LayerLink? startHandle, LayerLink? endHandle); /// Gets the selected content in this object. /// /// Return `null` if nothing is selected. SelectedContent? getSelectedContent(); /// Handles the [SelectionEvent] sent to this object. /// /// The subclasses need to update their selections or delegate the /// [SelectionEvent]s to their subtrees. /// /// The `event`s are subclasses of [SelectionEvent]. Check /// [SelectionEvent.type] to determine what kinds of event are dispatched to /// this handler and handle them accordingly. /// /// See also: /// * [SelectionEventType], which contains all of the possible types. SelectionResult dispatchSelectionEvent(SelectionEvent event); } /// The selected content in a [Selectable] or [SelectionHandler]. // TODO(chunhtai): Add more support for rich content. // https://github.com/flutter/flutter/issues/104206. class SelectedContent { /// Creates a selected content object. /// /// Only supports plain text. const SelectedContent({required this.plainText}); /// The selected content in plain text format. final String plainText; } /// A mixin that can be selected by users when under a [SelectionArea] widget. /// /// This object receives selection events and the [value] must reflect the /// current selection in this [Selectable]. The object must also notify its /// listener if the [value] ever changes. /// /// This object is responsible for drawing the selection highlight. /// /// In order to receive the selection event, the mixer needs to register /// itself to [SelectionRegistrar]s. Use /// [SelectionContainer.maybeOf] to get the selection registrar, and /// mix the [SelectionRegistrant] to subscribe to the [SelectionRegistrar] /// automatically. /// /// This mixin is typically mixed by [RenderObject]s. The [RenderObject.paint] /// methods are responsible to push the [LayerLink]s provided to /// [pushHandleLayers]. /// /// {@macro flutter.rendering.SelectionHandler} /// /// See also: /// * [SelectionArea], which provides an overview of selection system. mixin Selectable implements SelectionHandler { /// {@macro flutter.rendering.RenderObject.getTransformTo} Matrix4 getTransformTo(RenderObject? ancestor); /// The size of this [Selectable]. Size get size; /// Disposes resources held by the mixer. void dispose(); } /// A mixin to auto-register the mixer to the [registrar]. /// /// To use this mixin, the mixer needs to set the [registrar] to the /// [SelectionRegistrar] it wants to register to. /// /// This mixin only registers the mixer with the [registrar] if the /// [SelectionGeometry.hasContent] returned by the mixer is true. mixin SelectionRegistrant on Selectable { /// The [SelectionRegistrar] the mixer will be or is registered to. /// /// This [Selectable] only registers the mixer if the /// [SelectionGeometry.hasContent] returned by the [Selectable] is true. SelectionRegistrar? get registrar => _registrar; SelectionRegistrar? _registrar; set registrar(SelectionRegistrar? value) { if (value == _registrar) { return; } if (value == null) { // When registrar goes from non-null to null; removeListener(_updateSelectionRegistrarSubscription); } else if (_registrar == null) { // When registrar goes from null to non-null; addListener(_updateSelectionRegistrarSubscription); } _removeSelectionRegistrarSubscription(); _registrar = value; _updateSelectionRegistrarSubscription(); } @override void dispose() { _removeSelectionRegistrarSubscription(); super.dispose(); } bool _subscribedToSelectionRegistrar = false; void _updateSelectionRegistrarSubscription() { if (_registrar == null) { _subscribedToSelectionRegistrar = false; return; } if (_subscribedToSelectionRegistrar && !value.hasContent) { _registrar!.remove(this); _subscribedToSelectionRegistrar = false; } else if (!_subscribedToSelectionRegistrar && value.hasContent) { _registrar!.add(this); _subscribedToSelectionRegistrar = true; } } void _removeSelectionRegistrarSubscription() { if (_subscribedToSelectionRegistrar) { _registrar!.remove(this); _subscribedToSelectionRegistrar = false; } } } /// A utility class that provides useful methods for handling selection events. class SelectionUtils { SelectionUtils._(); /// Determines [SelectionResult] purely based on the target rectangle. /// /// This method returns [SelectionResult.end] if the `point` is inside the /// `targetRect`. Returns [SelectionResult.previous] if the `point` is /// considered to be lower than `targetRect` in screen order. Returns /// [SelectionResult.next] if the point is considered to be higher than /// `targetRect` in screen order. static SelectionResult getResultBasedOnRect(Rect targetRect, Offset point) { if (targetRect.contains(point)) { return SelectionResult.end; } if (point.dy < targetRect.top) { return SelectionResult.previous; } if (point.dy > targetRect.bottom) { return SelectionResult.next; } return point.dx >= targetRect.right ? SelectionResult.next : SelectionResult.previous; } /// Adjusts the dragging offset based on the target rect. /// /// This method moves the offsets to be within the target rect in case they are /// outside the rect. /// /// This is used in the case where a drag happens outside of the rectangle /// of a [Selectable]. /// /// The logic works as the following: /// ![](https://flutter.github.io/assets-for-api-docs/assets/rendering/adjust_drag_offset.png) /// /// For points inside the rect: /// Their effective locations are unchanged. /// /// For points in Area 1: /// Move them to top-left of the rect if text direction is ltr, or top-right /// if rtl. /// /// For points in Area 2: /// Move them to bottom-right of the rect if text direction is ltr, or /// bottom-left if rtl. static Offset adjustDragOffset(Rect targetRect, Offset point, {TextDirection direction = TextDirection.ltr}) { if (targetRect.contains(point)) { return point; } if (point.dy <= targetRect.top || point.dy <= targetRect.bottom && point.dx <= targetRect.left) { // Area 1 return direction == TextDirection.ltr ? targetRect.topLeft : targetRect.topRight; } else { // Area 2 return direction == TextDirection.ltr ? targetRect.bottomRight : targetRect.bottomLeft; } } } /// The type of a [SelectionEvent]. /// /// Used by [SelectionEvent.type] to distinguish different types of events. enum SelectionEventType { /// An event to update the selection start edge. /// /// Used by [SelectionEdgeUpdateEvent]. startEdgeUpdate, /// An event to update the selection end edge. /// /// Used by [SelectionEdgeUpdateEvent]. endEdgeUpdate, /// An event to clear the current selection. /// /// Used by [ClearSelectionEvent]. clear, /// An event to select all the available content. /// /// Used by [SelectAllSelectionEvent]. selectAll, /// An event to select a word at the location /// [SelectWordSelectionEvent.globalPosition]. /// /// Used by [SelectWordSelectionEvent]. selectWord, } /// An abstract base class for selection events. /// /// This should not be directly used. To handle a selection event, it should /// be downcast to a specific subclass. One can use [type] to look up which /// subclasses to downcast to. /// /// See also: /// * [SelectAllSelectionEvent], for events to select all contents. /// * [ClearSelectionEvent], for events to clear selections. /// * [SelectWordSelectionEvent], for events to select words at the locations. /// * [SelectionEdgeUpdateEvent], for events to update selection edges. /// * [SelectionEventType], for determining the subclass types. abstract class SelectionEvent { const SelectionEvent._(this.type); /// The type of this selection event. final SelectionEventType type; } /// Selects all selectable contents. /// /// This event can be sent as the result of keyboard select-all, i.e. /// ctrl + A, or cmd + A in macOS. class SelectAllSelectionEvent extends SelectionEvent { /// Creates a select all selection event. const SelectAllSelectionEvent(): super._(SelectionEventType.selectAll); } /// Clears the selection from the [Selectable] and removes any existing /// highlight as if there is no selection at all. class ClearSelectionEvent extends SelectionEvent { /// Create a clear selection event. const ClearSelectionEvent(): super._(SelectionEventType.clear); } /// Selects the whole word at the location. /// /// This event can be sent as the result of mobile long press selection. class SelectWordSelectionEvent extends SelectionEvent { /// Creates a select word event at the [globalPosition]. const SelectWordSelectionEvent({required this.globalPosition}): super._(SelectionEventType.selectWord); /// The position in global coordinates to select word at. final Offset globalPosition; } /// Updates a selection edge. /// /// An active selection contains two edges, start and end. Use the [type] to /// determine which edge this event applies to. If the [type] is /// [SelectionEventType.startEdgeUpdate], the event updates start edge. If the /// [type] is [SelectionEventType.endEdgeUpdate], the event updates end edge. /// /// The [globalPosition] contains the new offset of the edge. /// /// This event is dispatched when the framework detects [DragStartDetails] in /// [SelectionArea]'s gesture recognizers for mouse devices, or the selection /// handles have been dragged to new locations. class SelectionEdgeUpdateEvent extends SelectionEvent { /// Creates a selection start edge update event. /// /// The [globalPosition] contains the location of the selection start edge. const SelectionEdgeUpdateEvent.forStart({ required this.globalPosition }) : super._(SelectionEventType.startEdgeUpdate); /// Creates a selection end edge update event. /// /// The [globalPosition] contains the new location of the selection end edge. const SelectionEdgeUpdateEvent.forEnd({ required this.globalPosition }) : super._(SelectionEventType.endEdgeUpdate); /// The new location of the selection edge. final Offset globalPosition; } /// A registrar that keeps track of [Selectable]s in the subtree. /// /// A [Selectable] is only included in the [SelectableRegion] if they are /// registered with a [SelectionRegistrar]. Once a [Selectable] is registered, /// it will receive [SelectionEvent]s in /// [SelectionHandler.dispatchSelectionEvent]. /// /// Use [SelectionContainer.maybeOf] to get the immediate [SelectionRegistrar] /// in the ancestor chain above the build context. /// /// See also: /// * [SelectableRegion], which provides an overview of the selection system. /// * [SelectionRegistrarScope], which hosts the [SelectionRegistrar] for the /// subtree. /// * [SelectionRegistrant], which auto registers the object with the mixin to /// [SelectionRegistrar]. abstract class SelectionRegistrar { /// Adds the [selectable] into the registrar. /// /// A [Selectable] must register with the [SelectionRegistrar] in order to /// receive selection events. void add(Selectable selectable); /// Removes the [selectable] from the registrar. /// /// A [Selectable] must unregister itself if it is removed from the rendering /// tree. void remove(Selectable selectable); } /// The status that indicates whether there is a selection and whether the /// selection is collapsed. /// /// A collapsed selection means the selection starts and ends at the same /// location. enum SelectionStatus { /// The selection is not collapsed. /// /// For example if `{}` represent the selection edges: /// 'ab{cd}', the collapsing status is [uncollapsed]. /// '{abcd}', the collapsing status is [uncollapsed]. uncollapsed, /// The selection is collapsed. /// /// For example if `{}` represent the selection edges: /// 'ab{}cd', the collapsing status is [collapsed]. /// '{}abcd', the collapsing status is [collapsed]. /// 'abcd{}', the collapsing status is [collapsed]. collapsed, /// No selection. none, } /// The geometry of the current selection. /// /// This includes details such as the locations of the selection start and end, /// line height, etc. This information is used for drawing selection controls /// for mobile platforms. /// /// The positions in geometry are in local coordinates of the [SelectionHandler] /// or [Selectable]. @immutable class SelectionGeometry { /// Creates a selection geometry object. /// /// If any of the [startSelectionPoint] and [endSelectionPoint] is not null, /// the [status] must not be [SelectionStatus.none]. const SelectionGeometry({ this.startSelectionPoint, this.endSelectionPoint, required this.status, required this.hasContent, }) : assert((startSelectionPoint == null && endSelectionPoint == null) || status != SelectionStatus.none); /// The geometry information at the selection start. /// /// This information is used for drawing mobile selection controls. The /// [SelectionPoint.localPosition] of the selection start is typically at the /// start of the selection highlight at where the start selection handle /// should be drawn. /// /// The [SelectionPoint.handleType] should be [TextSelectionHandleType.left] /// for forward selection or [TextSelectionHandleType.right] for backward /// selection in most cases. /// /// Can be null if the selection start is offstage, for example, when the /// selection is outside of the viewport or is kept alive by a scrollable. final SelectionPoint? startSelectionPoint; /// The geometry information at the selection end. /// /// This information is used for drawing mobile selection controls. The /// [SelectionPoint.localPosition] of the selection end is typically at the end /// of the selection highlight at where the end selection handle should be /// drawn. /// /// The [SelectionPoint.handleType] should be [TextSelectionHandleType.right] /// for forward selection or [TextSelectionHandleType.left] for backward /// selection in most cases. /// /// Can be null if the selection end is offstage, for example, when the /// selection is outside of the viewport or is kept alive by a scrollable. final SelectionPoint? endSelectionPoint; /// The status of ongoing selection in the [Selectable] or [SelectionHandler]. final SelectionStatus status; /// Whether there is any selectable content in the [Selectable] or /// [SelectionHandler]. final bool hasContent; /// Whether there is an ongoing selection. bool get hasSelection => status != SelectionStatus.none; /// Makes a copy of this object with the given values updated. SelectionGeometry copyWith({ SelectionPoint? startSelectionPoint, SelectionPoint? endSelectionPoint, SelectionStatus? status, bool? hasContent, }) { return SelectionGeometry( startSelectionPoint: startSelectionPoint ?? this.startSelectionPoint, endSelectionPoint: endSelectionPoint ?? this.endSelectionPoint, status: status ?? this.status, hasContent: hasContent ?? this.hasContent, ); } @override bool operator ==(Object other) { if (identical(this, other)) { return true; } if (other.runtimeType != runtimeType) { return false; } return other is SelectionGeometry && other.startSelectionPoint == startSelectionPoint && other.endSelectionPoint == endSelectionPoint && other.status == status && other.hasContent == hasContent; } @override int get hashCode { return Object.hash( startSelectionPoint, endSelectionPoint, status, hasContent, ); } } /// The geometry information of a selection point. @immutable class SelectionPoint { /// Creates a selection point object. /// /// All properties must not be null. const SelectionPoint({ required this.localPosition, required this.lineHeight, required this.handleType, }) : assert(localPosition != null), assert(lineHeight != null), assert(handleType != null); /// The position of the selection point in the local coordinates of the /// containing [Selectable]. final Offset localPosition; /// The line height at the selection point. final double lineHeight; /// The selection handle type that should be used at the selection point. /// /// This is used for building the mobile selection handle. final TextSelectionHandleType handleType; @override bool operator ==(Object other) { if (identical(this, other)) { return true; } if (other.runtimeType != runtimeType) { return false; } return other is SelectionPoint && other.localPosition == localPosition && other.lineHeight == lineHeight && other.handleType == handleType; } @override int get hashCode { return Object.hash( localPosition, lineHeight, handleType, ); } } /// The type of selection handle to be displayed. /// /// With mixed-direction text, both handles may be the same type. Examples: /// /// * LTR text: 'the <quick brown> fox': /// /// The '<' is drawn with the [left] type, the '>' with the [right] /// /// * RTL text: 'XOF <NWORB KCIUQ> EHT': /// /// Same as above. /// /// * mixed text: '<the NWOR<B KCIUQ fox' /// /// Here 'the QUICK B' is selected, but 'QUICK BROWN' is RTL. Both are drawn /// with the [left] type. /// /// See also: /// /// * [TextDirection], which discusses left-to-right and right-to-left text in /// more detail. enum TextSelectionHandleType { /// The selection handle is to the left of the selection end point. left, /// The selection handle is to the right of the selection end point. right, /// The start and end of the selection are co-incident at this point. collapsed, }