blob: 39c8caeb80de18c4703c112b32bc7eef9a90780b [file] [log] [blame]
// 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 'dart:ui' show TextAffinity, TextPosition, TextRange;
import 'package:flutter/foundation.dart';
export 'dart:ui' show TextAffinity, TextPosition;
/// A range of text that represents a selection.
@immutable
class TextSelection extends TextRange {
/// Creates a text selection.
///
/// The [baseOffset] and [extentOffset] arguments must not be null.
const TextSelection({
required this.baseOffset,
required this.extentOffset,
this.affinity = TextAffinity.downstream,
this.isDirectional = false,
}) : super(
start: baseOffset < extentOffset ? baseOffset : extentOffset,
end: baseOffset < extentOffset ? extentOffset : baseOffset,
);
/// Creates a collapsed selection at the given offset.
///
/// A collapsed selection starts and ends at the same offset, which means it
/// contains zero characters but instead serves as an insertion point in the
/// text.
///
/// The [offset] argument must not be null.
const TextSelection.collapsed({
required int offset,
this.affinity = TextAffinity.downstream,
}) : baseOffset = offset,
extentOffset = offset,
isDirectional = false,
super.collapsed(offset);
/// Creates a collapsed selection at the given text position.
///
/// A collapsed selection starts and ends at the same offset, which means it
/// contains zero characters but instead serves as an insertion point in the
/// text.
TextSelection.fromPosition(TextPosition position)
: baseOffset = position.offset,
extentOffset = position.offset,
affinity = position.affinity,
isDirectional = false,
super.collapsed(position.offset);
/// The offset at which the selection originates.
///
/// Might be larger than, smaller than, or equal to extent.
final int baseOffset;
/// The offset at which the selection terminates.
///
/// When the user uses the arrow keys to adjust the selection, this is the
/// value that changes. Similarly, if the current theme paints a caret on one
/// side of the selection, this is the location at which to paint the caret.
///
/// Might be larger than, smaller than, or equal to base.
final int extentOffset;
/// If the text range is collapsed and has more than one visual location
/// (e.g., occurs at a line break), which of the two locations to use when
/// painting the caret.
final TextAffinity affinity;
/// Whether this selection has disambiguated its base and extent.
///
/// On some platforms, the base and extent are not disambiguated until the
/// first time the user adjusts the selection. At that point, either the start
/// or the end of the selection becomes the base and the other one becomes the
/// extent and is adjusted.
final bool isDirectional;
/// The position at which the selection originates.
///
/// {@template flutter.services.TextSelection.TextAffinity}
/// The [TextAffinity] of the resulting [TextPosition] is based on the
/// relative logical position in the text to the other selection endpoint:
/// * if [baseOffset] < [extentOffset], [base] will have
/// [TextAffinity.downstream] and [extent] will have
/// [TextAffinity.upstream].
/// * if [baseOffset] > [extentOffset], [base] will have
/// [TextAffinity.upstream] and [extent] will have
/// [TextAffinity.downstream].
/// * if [baseOffset] == [extentOffset], [base] and [extent] will both have
/// the collapsed selection's [affinity].
/// {@endtemplate}
///
/// Might be larger than, smaller than, or equal to extent.
TextPosition get base {
final TextAffinity affinity;
if (!isValid || baseOffset == extentOffset) {
affinity = this.affinity;
} else if (baseOffset < extentOffset) {
affinity = TextAffinity.downstream;
} else {
affinity = TextAffinity.upstream;
}
return TextPosition(offset: baseOffset, affinity: affinity);
}
/// The position at which the selection terminates.
///
/// When the user uses the arrow keys to adjust the selection, this is the
/// value that changes. Similarly, if the current theme paints a caret on one
/// side of the selection, this is the location at which to paint the caret.
///
/// {@macro flutter.services.TextSelection.TextAffinity}
///
/// Might be larger than, smaller than, or equal to base.
TextPosition get extent {
final TextAffinity affinity;
if (!isValid || baseOffset == extentOffset) {
affinity = this.affinity;
} else if (baseOffset < extentOffset) {
affinity = TextAffinity.upstream;
} else {
affinity = TextAffinity.downstream;
}
return TextPosition(offset: extentOffset, affinity: affinity);
}
@override
String toString() {
final String typeName = objectRuntimeType(this, 'TextSelection');
if (!isValid) {
return '$typeName.invalid';
}
return isCollapsed
? '$typeName.collapsed(offset: $baseOffset, affinity: $affinity, isDirectional: $isDirectional)'
: '$typeName(baseOffset: $baseOffset, extentOffset: $extentOffset, isDirectional: $isDirectional)';
}
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (other is! TextSelection) {
return false;
}
if (!isValid) {
return !other.isValid;
}
return other.baseOffset == baseOffset
&& other.extentOffset == extentOffset
&& (!isCollapsed || other.affinity == affinity)
&& other.isDirectional == isDirectional;
}
@override
int get hashCode {
if (!isValid) {
return Object.hash(-1.hashCode, -1.hashCode, TextAffinity.downstream.hashCode);
}
final int affinityHash = isCollapsed ? affinity.hashCode : TextAffinity.downstream.hashCode;
return Object.hash(baseOffset.hashCode, extentOffset.hashCode, affinityHash, isDirectional.hashCode);
}
/// Creates a new [TextSelection] based on the current selection, with the
/// provided parameters overridden.
TextSelection copyWith({
int? baseOffset,
int? extentOffset,
TextAffinity? affinity,
bool? isDirectional,
}) {
return TextSelection(
baseOffset: baseOffset ?? this.baseOffset,
extentOffset: extentOffset ?? this.extentOffset,
affinity: affinity ?? this.affinity,
isDirectional: isDirectional ?? this.isDirectional,
);
}
/// Returns the smallest [TextSelection] that this could expand to in order to
/// include the given [TextPosition].
///
/// If the given [TextPosition] is already inside of the selection, then
/// returns `this` without change.
///
/// The returned selection will always be a strict superset of the current
/// selection. In other words, the selection grows to include the given
/// [TextPosition].
///
/// If extentAtIndex is true, then the [TextSelection.extentOffset] will be
/// placed at the given index regardless of the original order of it and
/// [TextSelection.baseOffset]. Otherwise, their order will be preserved.
///
/// ## Difference with [extendTo]
/// In contrast with this method, [extendTo] is a pivot; it holds
/// [TextSelection.baseOffset] fixed while moving [TextSelection.extentOffset]
/// to the given [TextPosition]. It doesn't strictly grow the selection and
/// may collapse it or flip its order.
TextSelection expandTo(TextPosition position, [bool extentAtIndex = false]) {
// If position is already within in the selection, there's nothing to do.
if (position.offset >= start && position.offset <= end) {
return this;
}
final bool normalized = baseOffset <= extentOffset;
if (position.offset <= start) {
// Here the position is somewhere before the selection: ..|..[...]....
if (extentAtIndex) {
return copyWith(
baseOffset: end,
extentOffset: position.offset,
affinity: position.affinity,
);
}
return copyWith(
baseOffset: normalized ? position.offset : baseOffset,
extentOffset: normalized ? extentOffset : position.offset,
);
}
// Here the position is somewhere after the selection: ....[...]..|..
if (extentAtIndex) {
return copyWith(
baseOffset: start,
extentOffset: position.offset,
affinity: position.affinity,
);
}
return copyWith(
baseOffset: normalized ? baseOffset : position.offset,
extentOffset: normalized ? position.offset : extentOffset,
);
}
/// Keeping the selection's [TextSelection.baseOffset] fixed, pivot the
/// [TextSelection.extentOffset] to the given [TextPosition].
///
/// In some cases, the [TextSelection.baseOffset] and
/// [TextSelection.extentOffset] may flip during this operation, and/or the
/// size of the selection may shrink.
///
/// ## Difference with [expandTo]
/// In contrast with this method, [expandTo] is strictly growth; the
/// selection is grown to include the given [TextPosition] and will never
/// shrink.
TextSelection extendTo(TextPosition position) {
// If the selection's extent is at the position already, then nothing
// happens.
if (extent == position) {
return this;
}
return copyWith(
extentOffset: position.offset,
affinity: position.affinity,
);
}
}