blob: 7bbc0517c696490744d93ccfda6b81f508cbef76 [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' as ui show lerpDouble;
import 'package:flutter/foundation.dart';
import 'basic_types.dart';
/// Base class for [Alignment] that allows for text-direction aware
/// resolution.
///
/// A property or argument of this type accepts classes created either with [
/// Alignment] and its variants, or [AlignmentDirectional.new].
///
/// To convert an [AlignmentGeometry] object of indeterminate type into an
/// [Alignment] object, call the [resolve] method.
@immutable
abstract class AlignmentGeometry {
/// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
const AlignmentGeometry();
double get _x;
double get _start;
double get _y;
/// Returns the sum of two [AlignmentGeometry] objects.
///
/// If you know you are adding two [Alignment] or two [AlignmentDirectional]
/// objects, consider using the `+` operator instead, which always returns an
/// object of the same type as the operands, and is typed accordingly.
///
/// If [add] is applied to two objects of the same type ([Alignment] or
/// [AlignmentDirectional]), an object of that type will be returned (though
/// this is not reflected in the type system). Otherwise, an object
/// representing a combination of both is returned. That object can be turned
/// into a concrete [Alignment] using [resolve].
AlignmentGeometry add(AlignmentGeometry other) {
return _MixedAlignment(
_x + other._x,
_start + other._start,
_y + other._y,
);
}
/// Returns the negation of the given [AlignmentGeometry] object.
///
/// This is the same as multiplying the object by -1.0.
///
/// This operator returns an object of the same type as the operand.
AlignmentGeometry operator -();
/// Scales the [AlignmentGeometry] object in each dimension by the given factor.
///
/// This operator returns an object of the same type as the operand.
AlignmentGeometry operator *(double other);
/// Divides the [AlignmentGeometry] object in each dimension by the given factor.
///
/// This operator returns an object of the same type as the operand.
AlignmentGeometry operator /(double other);
/// Integer divides the [AlignmentGeometry] object in each dimension by the given factor.
///
/// This operator returns an object of the same type as the operand.
AlignmentGeometry operator ~/(double other);
/// Computes the remainder in each dimension by the given factor.
///
/// This operator returns an object of the same type as the operand.
AlignmentGeometry operator %(double other);
/// Linearly interpolate between two [AlignmentGeometry] objects.
///
/// If either is null, this function interpolates from [Alignment.center], and
/// the result is an object of the same type as the non-null argument.
///
/// If [lerp] is applied to two objects of the same type ([Alignment] or
/// [AlignmentDirectional]), an object of that type will be returned (though
/// this is not reflected in the type system). Otherwise, an object
/// representing a combination of both is returned. That object can be turned
/// into a concrete [Alignment] using [resolve].
///
/// {@macro dart.ui.shadow.lerp}
static AlignmentGeometry? lerp(AlignmentGeometry? a, AlignmentGeometry? b, double t) {
assert(t != null);
if (a == null && b == null) {
return null;
}
if (a == null) {
return b! * t;
}
if (b == null) {
return a * (1.0 - t);
}
if (a is Alignment && b is Alignment) {
return Alignment.lerp(a, b, t);
}
if (a is AlignmentDirectional && b is AlignmentDirectional) {
return AlignmentDirectional.lerp(a, b, t);
}
return _MixedAlignment(
ui.lerpDouble(a._x, b._x, t)!,
ui.lerpDouble(a._start, b._start, t)!,
ui.lerpDouble(a._y, b._y, t)!,
);
}
/// Convert this instance into an [Alignment], which uses literal
/// coordinates (the `x` coordinate being explicitly a distance from the
/// left).
///
/// See also:
///
/// * [Alignment], for which this is a no-op (returns itself).
/// * [AlignmentDirectional], which flips the horizontal direction
/// based on the `direction` argument.
Alignment resolve(TextDirection? direction);
@override
String toString() {
if (_start == 0.0) {
return Alignment._stringify(_x, _y);
}
if (_x == 0.0) {
return AlignmentDirectional._stringify(_start, _y);
}
return '${Alignment._stringify(_x, _y)} + ${AlignmentDirectional._stringify(_start, 0.0)}';
}
@override
bool operator ==(Object other) {
return other is AlignmentGeometry
&& other._x == _x
&& other._start == _start
&& other._y == _y;
}
@override
int get hashCode => Object.hash(_x, _start, _y);
}
/// A point within a rectangle.
///
/// `Alignment(0.0, 0.0)` represents the center of the rectangle. The distance
/// from -1.0 to +1.0 is the distance from one side of the rectangle to the
/// other side of the rectangle. Therefore, 2.0 units horizontally (or
/// vertically) is equivalent to the width (or height) of the rectangle.
///
/// `Alignment(-1.0, -1.0)` represents the top left of the rectangle.
///
/// `Alignment(1.0, 1.0)` represents the bottom right of the rectangle.
///
/// `Alignment(0.0, 3.0)` represents a point that is horizontally centered with
/// respect to the rectangle and vertically below the bottom of the rectangle by
/// the height of the rectangle.
///
/// `Alignment(0.0, -0.5)` represents a point that is horizontally centered with
/// respect to the rectangle and vertically half way between the top edge and
/// the center.
///
/// `Alignment(x, y)` in a rectangle with height h and width w describes
/// the point (x * w/2 + w/2, y * h/2 + h/2) in the coordinate system of the
/// rectangle.
///
/// [Alignment] uses visual coordinates, which means increasing [x] moves the
/// point from left to right. To support layouts with a right-to-left
/// [TextDirection], consider using [AlignmentDirectional], in which the
/// direction the point moves when increasing the horizontal value depends on
/// the [TextDirection].
///
/// A variety of widgets use [Alignment] in their configuration, most
/// notably:
///
/// * [Align] positions a child according to an [Alignment].
///
/// See also:
///
/// * [AlignmentDirectional], which has a horizontal coordinate orientation
/// that depends on the [TextDirection].
/// * [AlignmentGeometry], which is an abstract type that is agnostic as to
/// whether the horizontal direction depends on the [TextDirection].
class Alignment extends AlignmentGeometry {
/// Creates an alignment.
///
/// The [x] and [y] arguments must not be null.
const Alignment(this.x, this.y)
: assert(x != null),
assert(y != null);
/// The distance fraction in the horizontal direction.
///
/// A value of -1.0 corresponds to the leftmost edge. A value of 1.0
/// corresponds to the rightmost edge. Values are not limited to that range;
/// values less than -1.0 represent positions to the left of the left edge,
/// and values greater than 1.0 represent positions to the right of the right
/// edge.
final double x;
/// The distance fraction in the vertical direction.
///
/// A value of -1.0 corresponds to the topmost edge. A value of 1.0
/// corresponds to the bottommost edge. Values are not limited to that range;
/// values less than -1.0 represent positions above the top, and values
/// greater than 1.0 represent positions below the bottom.
final double y;
@override
double get _x => x;
@override
double get _start => 0.0;
@override
double get _y => y;
/// The top left corner.
static const Alignment topLeft = Alignment(-1.0, -1.0);
/// The center point along the top edge.
static const Alignment topCenter = Alignment(0.0, -1.0);
/// The top right corner.
static const Alignment topRight = Alignment(1.0, -1.0);
/// The center point along the left edge.
static const Alignment centerLeft = Alignment(-1.0, 0.0);
/// The center point, both horizontally and vertically.
static const Alignment center = Alignment(0.0, 0.0);
/// The center point along the right edge.
static const Alignment centerRight = Alignment(1.0, 0.0);
/// The bottom left corner.
static const Alignment bottomLeft = Alignment(-1.0, 1.0);
/// The center point along the bottom edge.
static const Alignment bottomCenter = Alignment(0.0, 1.0);
/// The bottom right corner.
static const Alignment bottomRight = Alignment(1.0, 1.0);
@override
AlignmentGeometry add(AlignmentGeometry other) {
if (other is Alignment) {
return this + other;
}
return super.add(other);
}
/// Returns the difference between two [Alignment]s.
Alignment operator -(Alignment other) {
return Alignment(x - other.x, y - other.y);
}
/// Returns the sum of two [Alignment]s.
Alignment operator +(Alignment other) {
return Alignment(x + other.x, y + other.y);
}
/// Returns the negation of the given [Alignment].
@override
Alignment operator -() {
return Alignment(-x, -y);
}
/// Scales the [Alignment] in each dimension by the given factor.
@override
Alignment operator *(double other) {
return Alignment(x * other, y * other);
}
/// Divides the [Alignment] in each dimension by the given factor.
@override
Alignment operator /(double other) {
return Alignment(x / other, y / other);
}
/// Integer divides the [Alignment] in each dimension by the given factor.
@override
Alignment operator ~/(double other) {
return Alignment((x ~/ other).toDouble(), (y ~/ other).toDouble());
}
/// Computes the remainder in each dimension by the given factor.
@override
Alignment operator %(double other) {
return Alignment(x % other, y % other);
}
/// Returns the offset that is this fraction in the direction of the given offset.
Offset alongOffset(Offset other) {
final double centerX = other.dx / 2.0;
final double centerY = other.dy / 2.0;
return Offset(centerX + x * centerX, centerY + y * centerY);
}
/// Returns the offset that is this fraction within the given size.
Offset alongSize(Size other) {
final double centerX = other.width / 2.0;
final double centerY = other.height / 2.0;
return Offset(centerX + x * centerX, centerY + y * centerY);
}
/// Returns the point that is this fraction within the given rect.
Offset withinRect(Rect rect) {
final double halfWidth = rect.width / 2.0;
final double halfHeight = rect.height / 2.0;
return Offset(
rect.left + halfWidth + x * halfWidth,
rect.top + halfHeight + y * halfHeight,
);
}
/// Returns a rect of the given size, aligned within given rect as specified
/// by this alignment.
///
/// For example, a 100×100 size inscribed on a 200×200 rect using
/// [Alignment.topLeft] would be the 100×100 rect at the top left of
/// the 200×200 rect.
Rect inscribe(Size size, Rect rect) {
final double halfWidthDelta = (rect.width - size.width) / 2.0;
final double halfHeightDelta = (rect.height - size.height) / 2.0;
return Rect.fromLTWH(
rect.left + halfWidthDelta + x * halfWidthDelta,
rect.top + halfHeightDelta + y * halfHeightDelta,
size.width,
size.height,
);
}
/// Linearly interpolate between two [Alignment]s.
///
/// If either is null, this function interpolates from [Alignment.center].
///
/// {@macro dart.ui.shadow.lerp}
static Alignment? lerp(Alignment? a, Alignment? b, double t) {
assert(t != null);
if (a == null && b == null) {
return null;
}
if (a == null) {
return Alignment(ui.lerpDouble(0.0, b!.x, t)!, ui.lerpDouble(0.0, b.y, t)!);
}
if (b == null) {
return Alignment(ui.lerpDouble(a.x, 0.0, t)!, ui.lerpDouble(a.y, 0.0, t)!);
}
return Alignment(ui.lerpDouble(a.x, b.x, t)!, ui.lerpDouble(a.y, b.y, t)!);
}
@override
Alignment resolve(TextDirection? direction) => this;
static String _stringify(double x, double y) {
if (x == -1.0 && y == -1.0) {
return 'Alignment.topLeft';
}
if (x == 0.0 && y == -1.0) {
return 'Alignment.topCenter';
}
if (x == 1.0 && y == -1.0) {
return 'Alignment.topRight';
}
if (x == -1.0 && y == 0.0) {
return 'Alignment.centerLeft';
}
if (x == 0.0 && y == 0.0) {
return 'Alignment.center';
}
if (x == 1.0 && y == 0.0) {
return 'Alignment.centerRight';
}
if (x == -1.0 && y == 1.0) {
return 'Alignment.bottomLeft';
}
if (x == 0.0 && y == 1.0) {
return 'Alignment.bottomCenter';
}
if (x == 1.0 && y == 1.0) {
return 'Alignment.bottomRight';
}
return 'Alignment(${x.toStringAsFixed(1)}, '
'${y.toStringAsFixed(1)})';
}
@override
String toString() => _stringify(x, y);
}
/// An offset that's expressed as a fraction of a [Size], but whose horizontal
/// component is dependent on the writing direction.
///
/// This can be used to indicate an offset from the left in [TextDirection.ltr]
/// text and an offset from the right in [TextDirection.rtl] text without having
/// to be aware of the current text direction.
///
/// See also:
///
/// * [Alignment], a variant that is defined in physical terms (i.e.
/// whose horizontal component does not depend on the text direction).
class AlignmentDirectional extends AlignmentGeometry {
/// Creates a directional alignment.
///
/// The [start] and [y] arguments must not be null.
const AlignmentDirectional(this.start, this.y)
: assert(start != null),
assert(y != null);
/// The distance fraction in the horizontal direction.
///
/// A value of -1.0 corresponds to the edge on the "start" side, which is the
/// left side in [TextDirection.ltr] contexts and the right side in
/// [TextDirection.rtl] contexts. A value of 1.0 corresponds to the opposite
/// edge, the "end" side. Values are not limited to that range; values less
/// than -1.0 represent positions beyond the start edge, and values greater than
/// 1.0 represent positions beyond the end edge.
///
/// This value is normalized into an [Alignment.x] value by the [resolve]
/// method.
final double start;
/// The distance fraction in the vertical direction.
///
/// A value of -1.0 corresponds to the topmost edge. A value of 1.0
/// corresponds to the bottommost edge. Values are not limited to that range;
/// values less than -1.0 represent positions above the top, and values
/// greater than 1.0 represent positions below the bottom.
///
/// This value is passed through to [Alignment.y] unmodified by the
/// [resolve] method.
final double y;
@override
double get _x => 0.0;
@override
double get _start => start;
@override
double get _y => y;
/// The top corner on the "start" side.
static const AlignmentDirectional topStart = AlignmentDirectional(-1.0, -1.0);
/// The center point along the top edge.
///
/// Consider using [Alignment.topCenter] instead, as it does not need
/// to be [resolve]d to be used.
static const AlignmentDirectional topCenter = AlignmentDirectional(0.0, -1.0);
/// The top corner on the "end" side.
static const AlignmentDirectional topEnd = AlignmentDirectional(1.0, -1.0);
/// The center point along the "start" edge.
static const AlignmentDirectional centerStart = AlignmentDirectional(-1.0, 0.0);
/// The center point, both horizontally and vertically.
///
/// Consider using [Alignment.center] instead, as it does not need to
/// be [resolve]d to be used.
static const AlignmentDirectional center = AlignmentDirectional(0.0, 0.0);
/// The center point along the "end" edge.
static const AlignmentDirectional centerEnd = AlignmentDirectional(1.0, 0.0);
/// The bottom corner on the "start" side.
static const AlignmentDirectional bottomStart = AlignmentDirectional(-1.0, 1.0);
/// The center point along the bottom edge.
///
/// Consider using [Alignment.bottomCenter] instead, as it does not
/// need to be [resolve]d to be used.
static const AlignmentDirectional bottomCenter = AlignmentDirectional(0.0, 1.0);
/// The bottom corner on the "end" side.
static const AlignmentDirectional bottomEnd = AlignmentDirectional(1.0, 1.0);
@override
AlignmentGeometry add(AlignmentGeometry other) {
if (other is AlignmentDirectional) {
return this + other;
}
return super.add(other);
}
/// Returns the difference between two [AlignmentDirectional]s.
AlignmentDirectional operator -(AlignmentDirectional other) {
return AlignmentDirectional(start - other.start, y - other.y);
}
/// Returns the sum of two [AlignmentDirectional]s.
AlignmentDirectional operator +(AlignmentDirectional other) {
return AlignmentDirectional(start + other.start, y + other.y);
}
/// Returns the negation of the given [AlignmentDirectional].
@override
AlignmentDirectional operator -() {
return AlignmentDirectional(-start, -y);
}
/// Scales the [AlignmentDirectional] in each dimension by the given factor.
@override
AlignmentDirectional operator *(double other) {
return AlignmentDirectional(start * other, y * other);
}
/// Divides the [AlignmentDirectional] in each dimension by the given factor.
@override
AlignmentDirectional operator /(double other) {
return AlignmentDirectional(start / other, y / other);
}
/// Integer divides the [AlignmentDirectional] in each dimension by the given factor.
@override
AlignmentDirectional operator ~/(double other) {
return AlignmentDirectional((start ~/ other).toDouble(), (y ~/ other).toDouble());
}
/// Computes the remainder in each dimension by the given factor.
@override
AlignmentDirectional operator %(double other) {
return AlignmentDirectional(start % other, y % other);
}
/// Linearly interpolate between two [AlignmentDirectional]s.
///
/// If either is null, this function interpolates from [AlignmentDirectional.center].
///
/// {@macro dart.ui.shadow.lerp}
static AlignmentDirectional? lerp(AlignmentDirectional? a, AlignmentDirectional? b, double t) {
assert(t != null);
if (a == null && b == null) {
return null;
}
if (a == null) {
return AlignmentDirectional(ui.lerpDouble(0.0, b!.start, t)!, ui.lerpDouble(0.0, b.y, t)!);
}
if (b == null) {
return AlignmentDirectional(ui.lerpDouble(a.start, 0.0, t)!, ui.lerpDouble(a.y, 0.0, t)!);
}
return AlignmentDirectional(ui.lerpDouble(a.start, b.start, t)!, ui.lerpDouble(a.y, b.y, t)!);
}
@override
Alignment resolve(TextDirection? direction) {
assert(direction != null, 'Cannot resolve $runtimeType without a TextDirection.');
switch (direction!) {
case TextDirection.rtl:
return Alignment(-start, y);
case TextDirection.ltr:
return Alignment(start, y);
}
}
static String _stringify(double start, double y) {
if (start == -1.0 && y == -1.0) {
return 'AlignmentDirectional.topStart';
}
if (start == 0.0 && y == -1.0) {
return 'AlignmentDirectional.topCenter';
}
if (start == 1.0 && y == -1.0) {
return 'AlignmentDirectional.topEnd';
}
if (start == -1.0 && y == 0.0) {
return 'AlignmentDirectional.centerStart';
}
if (start == 0.0 && y == 0.0) {
return 'AlignmentDirectional.center';
}
if (start == 1.0 && y == 0.0) {
return 'AlignmentDirectional.centerEnd';
}
if (start == -1.0 && y == 1.0) {
return 'AlignmentDirectional.bottomStart';
}
if (start == 0.0 && y == 1.0) {
return 'AlignmentDirectional.bottomCenter';
}
if (start == 1.0 && y == 1.0) {
return 'AlignmentDirectional.bottomEnd';
}
return 'AlignmentDirectional(${start.toStringAsFixed(1)}, '
'${y.toStringAsFixed(1)})';
}
@override
String toString() => _stringify(start, y);
}
class _MixedAlignment extends AlignmentGeometry {
const _MixedAlignment(this._x, this._start, this._y);
@override
final double _x;
@override
final double _start;
@override
final double _y;
@override
_MixedAlignment operator -() {
return _MixedAlignment(
-_x,
-_start,
-_y,
);
}
@override
_MixedAlignment operator *(double other) {
return _MixedAlignment(
_x * other,
_start * other,
_y * other,
);
}
@override
_MixedAlignment operator /(double other) {
return _MixedAlignment(
_x / other,
_start / other,
_y / other,
);
}
@override
_MixedAlignment operator ~/(double other) {
return _MixedAlignment(
(_x ~/ other).toDouble(),
(_start ~/ other).toDouble(),
(_y ~/ other).toDouble(),
);
}
@override
_MixedAlignment operator %(double other) {
return _MixedAlignment(
_x % other,
_start % other,
_y % other,
);
}
@override
Alignment resolve(TextDirection? direction) {
assert(direction != null, 'Cannot resolve $runtimeType without a TextDirection.');
switch (direction!) {
case TextDirection.rtl:
return Alignment(_x - _start, _y);
case TextDirection.ltr:
return Alignment(_x + _start, _y);
}
}
}
/// The vertical alignment of text within an input box.
///
/// A single [y] value that can range from -1.0 to 1.0. -1.0 aligns to the top
/// of an input box so that the top of the first line of text fits within the
/// box and its padding. 0.0 aligns to the center of the box. 1.0 aligns so that
/// the bottom of the last line of text aligns with the bottom interior edge of
/// the input box.
///
/// See also:
///
/// * [TextField.textAlignVertical], which is passed on to the [InputDecorator].
/// * [CupertinoTextField.textAlignVertical], which behaves in the same way as
/// the parameter in TextField.
/// * [InputDecorator.textAlignVertical], which defines the alignment of
/// prefix, input, and suffix within an [InputDecorator].
class TextAlignVertical {
/// Creates a TextAlignVertical from any y value between -1.0 and 1.0.
const TextAlignVertical({
required this.y,
}) : assert(y != null),
assert(y >= -1.0 && y <= 1.0);
/// A value ranging from -1.0 to 1.0 that defines the topmost and bottommost
/// locations of the top and bottom of the input box.
final double y;
/// Aligns a TextField's input Text with the topmost location within a
/// TextField's input box.
static const TextAlignVertical top = TextAlignVertical(y: -1.0);
/// Aligns a TextField's input Text to the center of the TextField.
static const TextAlignVertical center = TextAlignVertical(y: 0.0);
/// Aligns a TextField's input Text with the bottommost location within a
/// TextField.
static const TextAlignVertical bottom = TextAlignVertical(y: 1.0);
@override
String toString() {
return '${objectRuntimeType(this, 'TextAlignVertical')}(y: $y)';
}
}