257 lines
9.1 KiB
Dart
257 lines
9.1 KiB
Dart
// 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.
|
|
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.
|
|
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 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);
|
|
}
|
|
}
|