From f235a2c104d8bbbdcdcab1716c366e9cfa3e4787 Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Mon, 28 Aug 2017 12:50:24 -0700 Subject: [PATCH] RTL: Padding, Flex (#11709) * Introduce a Directionality inherited widget which sets the ambient LTR vs RTL mode (defaulting to null, which means you cannot use directionality-influenced values). * Make it possible to configure Padding (including Container.padding and Container.margin) using a directionality-agnostic EdgeInsets variant. * Provide textDirection and verticalDirection controls on Row and Column to make them RTL-aware. * Introduce a variant of FractionalOffset based on the EdgeInsets variant. Not yet actually used. * Fix all the tests that depended on Row defaulting to LTR. --- examples/layers/test/gestures_test.dart | 5 +- packages/flutter/lib/src/material/app.dart | 11 +- packages/flutter/lib/src/material/chip.dart | 12 +- .../src/material/material_localizations.dart | 54 +- packages/flutter/lib/src/material/tabs.dart | 10 + .../flutter/lib/src/painting/basic_types.dart | 67 ++ .../flutter/lib/src/painting/edge_insets.dart | 712 +++++++++-- .../lib/src/painting/fractional_offset.dart | 542 ++++++++- packages/flutter/lib/src/rendering/box.dart | 10 + packages/flutter/lib/src/rendering/flex.dart | 233 +++- packages/flutter/lib/src/rendering/node.dart | 2 +- .../lib/src/rendering/shifted_box.dart | 78 +- .../flutter/lib/src/rendering/sliver.dart | 4 + packages/flutter/lib/src/widgets/app.dart | 23 +- packages/flutter/lib/src/widgets/basic.dart | 167 ++- .../flutter/lib/src/widgets/container.dart | 16 +- .../lib/src/widgets/localizations.dart | 84 +- .../lib/src/widgets/scroll_controller.dart | 2 +- .../test/cupertino/bottom_tab_bar_test.dart | 22 +- .../flutter/test/material/about_test.dart | 5 +- .../flutter/test/material/app_bar_test.dart | 126 +- .../test/material/button_bar_test.dart | 9 +- packages/flutter/test/material/chip_test.dart | 158 ++- .../test/material/control_list_tile_test.dart | 15 +- .../test/material/expansion_panel_test.dart | 72 +- .../test/material/grid_title_test.dart | 34 +- .../test/material/icon_button_test.dart | 21 +- .../test/material/input_decorator_test.dart | 40 +- .../test/material/page_selector_test.dart | 51 +- .../flutter/test/material/stepper_test.dart | 544 +++++---- packages/flutter/test/material/tabs_test.dart | 31 +- .../test/material/text_field_test.dart | 1063 +++++++---------- .../test/painting/box_painter_test.dart | 2 +- .../test/painting/edge_insets_test.dart | 137 +++ .../test/painting/fractional_offset_test.dart | 109 +- .../flutter/test/rendering/flex_test.dart | 175 ++- .../test/rendering/limited_box_test.dart | 6 +- .../test/rendering/mutations_test.dart | 2 +- .../widgets/automatic_keep_alive_test.dart | 232 ++-- .../test/widgets/clamp_overscrolls_test.dart | 1 + .../flutter/test/widgets/column_test.dart | 395 ++++++ .../flutter/test/widgets/container_test.dart | 8 +- packages/flutter/test/widgets/flex_test.dart | 2 + packages/flutter/test/widgets/focus_test.dart | 3 + .../flutter/test/widgets/framework_test.dart | 2 +- .../widgets/global_keys_duplicated_test.dart | 47 +- packages/flutter/test/widgets/image_test.dart | 2 + .../layout_builder_and_parent_data_test.dart | 1 + .../layout_builder_mutations_test.dart | 62 +- .../test/widgets/localizations_test.dart | 21 + .../test/widgets/nested_scroll_view_test.dart | 129 +- .../test/widgets/overflow_box_test.dart | 2 +- .../flutter/test/widgets/overlay_test.dart | 4 +- .../test/widgets/reparent_state_test.dart | 2 + .../test/widgets/rotated_box_test.dart | 1 + packages/flutter/test/widgets/row_test.dart | 871 +++++++++++++- packages/flutter/test/widgets/rtl_test.dart | 70 ++ packages/flutter_test/lib/src/binding.dart | 2 +- .../flutter_test/test/widget_tester_test.dart | 53 +- 59 files changed, 4926 insertions(+), 1638 deletions(-) create mode 100644 packages/flutter/test/widgets/rtl_test.dart diff --git a/examples/layers/test/gestures_test.dart b/examples/layers/test/gestures_test.dart index 3d82e2aaa4..902385a43c 100644 --- a/examples/layers/test/gestures_test.dart +++ b/examples/layers/test/gestures_test.dart @@ -9,7 +9,10 @@ import '../widgets/gestures.dart'; void main() { testWidgets('Tap on center change color', (WidgetTester tester) async { - await tester.pumpWidget(new GestureDemo()); + await tester.pumpWidget(new Directionality( + textDirection: TextDirection.ltr, + child: new GestureDemo(), + )); final Finder finder = find.byType(GestureDemo); MaterialColor getSwatch() => tester.state(finder).swatch; diff --git a/packages/flutter/lib/src/material/app.dart b/packages/flutter/lib/src/material/app.dart index efe0ffd570..a11462219a 100644 --- a/packages/flutter/lib/src/material/app.dart +++ b/packages/flutter/lib/src/material/app.dart @@ -26,11 +26,14 @@ const TextStyle _errorTextStyle = const TextStyle( decorationStyle: TextDecorationStyle.double ); +// Delegate that fetches the default (English) strings. class _MaterialLocalizationsDelegate extends LocalizationsDelegate { const _MaterialLocalizationsDelegate(); @override - Future load(Locale locale) => MaterialLocalizations.load(locale); + Future load(Locale locale) { + return new SynchronousFuture(const MaterialLocalizations()); + } @override bool shouldReload(_MaterialLocalizationsDelegate old) => false; @@ -317,8 +320,8 @@ class _MaterialAppState extends State { // Combine the Localizations for Material with the ones contributed // by the localizationsDelegates parameter, if any. - Iterable> _createLocalizationsDelegates() sync* { - yield const _MaterialLocalizationsDelegate(); + Iterable> get _localizationsDelegates sync* { + yield const _MaterialLocalizationsDelegate(); // TODO(ianh): make this configurable if (widget.localizationsDelegates != null) yield* widget.localizationsDelegates; } @@ -397,7 +400,7 @@ class _MaterialAppState extends State { onGenerateRoute: _onGenerateRoute, onUnknownRoute: _onUnknownRoute, locale: widget.locale, - localizationsDelegates: _createLocalizationsDelegates(), + localizationsDelegates: _localizationsDelegates, showPerformanceOverlay: widget.showPerformanceOverlay, checkerboardRasterCacheImages: widget.checkerboardRasterCacheImages, checkerboardOffscreenLayers: widget.checkerboardOffscreenLayers, diff --git a/packages/flutter/lib/src/material/chip.dart b/packages/flutter/lib/src/material/chip.dart index cb98ad2383..e611fa68e4 100644 --- a/packages/flutter/lib/src/material/chip.dart +++ b/packages/flutter/lib/src/material/chip.dart @@ -106,16 +106,16 @@ class Chip extends StatelessWidget { Widget build(BuildContext context) { assert(debugCheckHasMaterial(context)); final bool deletable = onDeleted != null; - double leftPadding = 12.0; - double rightPadding = 12.0; + double startPadding = 12.0; + double endPadding = 12.0; final List children = []; if (avatar != null) { - leftPadding = 0.0; + startPadding = 0.0; children.add(new ExcludeSemantics( child: new Container( - margin: const EdgeInsets.only(right: 8.0), + margin: const EdgeInsetsDirectional.only(end: 8.0), width: _kAvatarDiamater, height: _kAvatarDiamater, child: avatar, @@ -131,7 +131,7 @@ class Chip extends StatelessWidget { )); if (deletable) { - rightPadding = 0.0; + endPadding = 0.0; children.add(new GestureDetector( onTap: Feedback.wrapForTap(onDeleted, context), child: new Tooltip( @@ -152,7 +152,7 @@ class Chip extends StatelessWidget { container: true, child: new Container( height: _kChipHeight, - padding: new EdgeInsets.only(left: leftPadding, right: rightPadding), + padding: new EdgeInsetsDirectional.only(start: startPadding, end: endPadding), decoration: new BoxDecoration( color: backgroundColor ?? Colors.grey.shade300, borderRadius: new BorderRadius.circular(16.0), diff --git a/packages/flutter/lib/src/material/material_localizations.dart b/packages/flutter/lib/src/material/material_localizations.dart index b086084974..b13adce5ac 100644 --- a/packages/flutter/lib/src/material/material_localizations.dart +++ b/packages/flutter/lib/src/material/material_localizations.dart @@ -2,44 +2,20 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; - -import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; -/// Default localized resource values for the material widgets. +/// Interface for localized resource values for the material widgets. /// -/// This class is just a placeholder, it only provides English values. +/// This class provides a default placeholder implementation that returns +/// hard-coded American English values. class MaterialLocalizations { - const MaterialLocalizations._(this.locale) : assert(locale != null); + /// Create a placeholder object for the localized resources of material widgets + /// which only provides American English strings. + const MaterialLocalizations(); /// The locale for which the values of this class's localized resources /// have been translated. - final Locale locale; - - /// Creates an object that provides default localized resource values for the - /// for the widgets of the material library. - /// - /// This method is typically used to create a [DefaultLocalizationsDelegate]. - /// The [MaterialApp] does so by default. - static Future load(Locale locale) { - return new SynchronousFuture(new MaterialLocalizations._(locale)); - } - - /// The `MaterialLocalizations` from the closest [Localizations] instance - /// that encloses the given context. - /// - /// This method is just a convenient shorthand for: - /// `Localizations.of(context, MaterialLocalizations)`. - /// - /// References to the localized resources defined by this class are typically - /// written in terms of this method. For example: - /// ```dart - /// tooltip: MaterialLocalizations.of(context).backButtonTooltip, - /// ``` - static MaterialLocalizations of(BuildContext context) { - return Localizations.of(context, MaterialLocalizations); - } + Locale get locale => const Locale('en', 'US'); /// The tooltip for the leading [AppBar] menu (aka 'hamburger') button String get openAppDrawerTooltip => 'Open navigation menu'; @@ -55,4 +31,20 @@ class MaterialLocalizations { /// The tooltip for the [MonthPicker]'s "previous month" button. String get previousMonthTooltip => 'Previous month'; + + /// The `MaterialLocalizations` from the closest [Localizations] instance + /// that encloses the given context. + /// + /// This method is just a convenient shorthand for: + /// `Localizations.of(context, MaterialLocalizations)`. + /// + /// References to the localized resources defined by this class are typically + /// written in terms of this method. For example: + /// + /// ```dart + /// tooltip: MaterialLocalizations.of(context).backButtonTooltip, + /// ``` + static MaterialLocalizations of(BuildContext context) { + return Localizations.of(context, MaterialLocalizations); + } } diff --git a/packages/flutter/lib/src/material/tabs.dart b/packages/flutter/lib/src/material/tabs.dart index 7968a850ac..76dd2c0979 100644 --- a/packages/flutter/lib/src/material/tabs.dart +++ b/packages/flutter/lib/src/material/tabs.dart @@ -150,6 +150,8 @@ class _TabLabelBarRenderer extends RenderFlex { MainAxisSize mainAxisSize, MainAxisAlignment mainAxisAlignment, CrossAxisAlignment crossAxisAlignment, + TextDirection textDirection, + VerticalDirection verticalDirection, TextBaseline textBaseline, @required this.onPerformLayout, }) : assert(onPerformLayout != null), @@ -159,6 +161,8 @@ class _TabLabelBarRenderer extends RenderFlex { mainAxisSize: mainAxisSize, mainAxisAlignment: mainAxisAlignment, crossAxisAlignment: crossAxisAlignment, + textDirection: textDirection, + verticalDirection: verticalDirection, textBaseline: textBaseline, ); @@ -188,6 +192,8 @@ class _TabLabelBar extends Flex { Key key, MainAxisAlignment mainAxisAlignment, CrossAxisAlignment crossAxisAlignment, + TextDirection textDirection, + VerticalDirection verticalDirection: VerticalDirection.down, List children: const [], this.onPerformLayout, }) : super( @@ -197,6 +203,8 @@ class _TabLabelBar extends Flex { mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, + textDirection: textDirection, + verticalDirection: verticalDirection, ); final ValueChanged> onPerformLayout; @@ -208,6 +216,8 @@ class _TabLabelBar extends Flex { mainAxisAlignment: mainAxisAlignment, mainAxisSize: mainAxisSize, crossAxisAlignment: crossAxisAlignment, + textDirection: getEffectiveTextDirection(context), + verticalDirection: verticalDirection, textBaseline: textBaseline, onPerformLayout: onPerformLayout, ); diff --git a/packages/flutter/lib/src/painting/basic_types.dart b/packages/flutter/lib/src/painting/basic_types.dart index 80d0465646..aaddbd7ae3 100644 --- a/packages/flutter/lib/src/painting/basic_types.dart +++ b/packages/flutter/lib/src/painting/basic_types.dart @@ -90,3 +90,70 @@ enum RenderComparison { /// change in a render object. layout, } + +/// The two cardinal directions in two dimensions. +/// +/// The axis is always relative to the current coordinate space. This means, for +/// example, that a [horizontal] axis might actually be diagonally from top +/// right to bottom left, due to some local [Transform] applied to the scene. +/// +/// See also: +/// +/// * [AxisDirection], which is a directional version of this enum (with values +/// light left and right, rather than just horizontal). +/// * [TextDirection], which disambiguates between left-to-right horizontal +/// content and right-to-left horizontal content. +enum Axis { + /// Left and right. + /// + /// See also: + /// + /// * [TextDirection], which disambiguates between left-to-right horizontal + /// content and right-to-left horizontal content. + horizontal, + + /// Up and down. + vertical, +} + +/// Returns the opposite of the given [Axis]. +/// +/// Specifically, returns [Axis.horizontal] for [Axis.vertical], and +/// vice versa. +/// +/// See also: +/// +/// * [flipAxisDirection], which does the same thing for [AxisDirection] values. +Axis flipAxis(Axis direction) { + assert(direction != null); + switch (direction) { + case Axis.horizontal: + return Axis.vertical; + case Axis.vertical: + return Axis.horizontal; + } + return null; +} + +/// A direction in which boxes flow vertically. +/// +/// This is used by the flex algorithm (e.g. [Column]) to decide in which +/// direction to draw boxes. +/// +/// This is also used to disambiguate `start` and `end` values (e.g. +/// [MainAxisAlignment.start] or [CrossAxisAlignment.end]). +/// +/// See also: +/// +/// * [TextDirection], which controls the same thing but horizontally. +enum VerticalDirection { + /// Boxes should start at the bottom and be stacked vertically towards the top. + /// + /// The "start" is at the bottom, the "end" is at the top. + up, + + /// Boxes should start at the top and be stacked vertically towards the bottom. + /// + /// The "start" is at the top, the "end" is at the bottom. + down, +} diff --git a/packages/flutter/lib/src/painting/edge_insets.dart b/packages/flutter/lib/src/painting/edge_insets.dart index c7521cd29d..1a8514afc2 100644 --- a/packages/flutter/lib/src/painting/edge_insets.dart +++ b/packages/flutter/lib/src/painting/edge_insets.dart @@ -8,13 +8,255 @@ import 'package:flutter/foundation.dart'; import 'basic_types.dart'; -/// The two cardinal directions in two dimensions. -enum Axis { - /// Left and right - horizontal, +/// Base class for [EdgeInsets] that allows for text-direction aware +/// resolution. +/// +/// A property or argument of this type accepts classes created either with [new +/// EdgeInsets.fromLTRB] and its variants, or [new EdgeInsetsDirectional]. +/// +/// To convert a [EdgeInsetsGeometry] object of indeterminate type into a +/// [EdgeInsets] object, call the [resolve] method. +/// +/// See also: +/// +/// * [Padding], a widget that describes margins using [EdgeInsetsGeometry]. +abstract class EdgeInsetsGeometry { + /// Abstract const constructor. This constructor enables subclasses to provide + /// const constructors so that they can be used in const expressions. + const EdgeInsetsGeometry(); - /// Up and down - vertical, + double get _bottom; + double get _end; + double get _left; + double get _right; + double get _start; + double get _top; + + /// Whether every dimension is non-negative. + bool get isNonNegative { + return _left >= 0.0 + && _right >= 0.0 + && _start >= 0.0 + && _end >= 0.0 + && _top >= 0.0 + && _bottom >= 0.0; + } + + /// The total offset in the vertical direction. + double get horizontal => _left + _right + _start + _end; + + /// The total offset in the horizontal direction. + double get vertical => _top + _bottom; + + /// The total offset in the given direction. + double along(Axis axis) { + assert(axis != null); + switch (axis) { + case Axis.horizontal: + return horizontal; + case Axis.vertical: + return vertical; + } + return null; + } + + /// The size that this [EdgeInsets] would occupy with an empty interior. + Size get collapsedSize => new Size(horizontal, vertical); + + /// An [EdgeInsetsGeometry] with top and bottom, left and right, and start and end flipped. + EdgeInsetsGeometry get flipped => new _MixedEdgeInsets.fromLRSETB(_right, _left, _end, _start, _bottom, _top); + + /// Returns a new size that is bigger than the given size by the amount of + /// inset in the horizontal and vertical directions. + /// + /// See also: + /// + /// * [EdgeInsets.inflateRect], to inflate a [Rect] rather than a [Size] (for + /// [EdgeInsetsDirectional], requires first calling [resolve] to establish + /// how the start and and map to the left or right). + /// * [deflateSize], to deflate a [Size] rather than inflating it. + Size inflateSize(Size size) { + return new Size(size.width + horizontal, size.height + vertical); + } + + /// Returns a new size that is smaller than the given size by the amount of + /// inset in the horizontal and vertical directions. + /// + /// If the argument is smaller than [collapsedSize], then the resulting size + /// will have negative dimensions. + /// + /// See also: + /// + /// * [EdgeInsets.deflateRect], to deflate a [Rect] rather than a [Size]. (for + /// [EdgeInsetsDirectional], requires first calling [resolve] to establish + /// how the start and and map to the left or right). + /// * [inflateSize], to inflate a [Size] rather than deflating it. + Size deflateSize(Size size) { + return new Size(size.width - horizontal, size.height - vertical); + } + + /// Returns the difference between two [EdgeInsetsGeometry] objects. + /// + /// If you know you are applying this to two [EdgeInsets] or two + /// [EdgeInsetsDirectional] objects, consider using the binary infix `-` + /// operator instead, which always returns an object of the same type as the + /// operands, and is typed accordingly. + /// + /// If [subtract] is applied to two objects of the same type ([EdgeInsets] or + /// [EdgeInsetsDirectional]), 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 [EdgeInsets] using [resolve]. + /// + /// This method returns the same result as [add] applied to the result of + /// negating the argument (using the prefix unary `-` operator or multiplying + /// the argument by -1.0 using the `*` operator). + EdgeInsetsGeometry subtract(EdgeInsetsGeometry other) { + return new _MixedEdgeInsets.fromLRSETB( + _left - other._left, + _right - other._right, + _start - other._start, + _end - other._end, + _top - other._top, + _bottom - other._bottom, + ); + } + + /// Returns the sum of two [EdgeInsetsGeometry] objects. + /// + /// If you know you are adding two [EdgeInsets] or two [EdgeInsetsDirectional] + /// 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 ([EdgeInsets] or + /// [EdgeInsetsDirectional]), 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 [EdgeInsets] using [resolve]. + EdgeInsetsGeometry add(EdgeInsetsGeometry other) { + return new _MixedEdgeInsets.fromLRSETB( + _left + other._left, + _right + other._right, + _start + other._start, + _end + other._end, + _top + other._top, + _bottom + other._bottom, + ); + } + + /// Returns the [EdgeInsetsGeometry] object with each dimension negated. + /// + /// This is the same as multiplying the object by -1.0. + /// + /// This operator returns an object of the same type as the operand. + EdgeInsetsGeometry operator -(); + + /// Scales the [EdgeInsetsGeometry] object in each dimension by the given factor. + /// + /// This operator returns an object of the same type as the operand. + EdgeInsetsGeometry operator *(double other); + + /// Divides the [EdgeInsetsGeometry] object in each dimension by the given factor. + /// + /// This operator returns an object of the same type as the operand. + EdgeInsetsGeometry operator /(double other); + + /// Integer divides the [EdgeInsetsGeometry] object in each dimension by the given factor. + /// + /// This operator returns an object of the same type as the operand. + EdgeInsetsGeometry 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. + EdgeInsetsGeometry operator %(double other); + + /// Linearly interpolate between two [EdgeInsetsGeometry] objects. + /// + /// If either is null, this function interpolates from [EdgeInsets.zero], 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 ([EdgeInsets] or + /// [EdgeInsetsDirectional]), 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 [EdgeInsets] using [resolve]. + static EdgeInsetsGeometry lerp(EdgeInsetsGeometry a, EdgeInsetsGeometry b, double t) { + if (a == null && b == null) + return null; + if (a == null) + return b * t; + if (b == null) + return a * (1.0 - t); + if (a is EdgeInsets && b is EdgeInsets) + return EdgeInsets.lerp(a, b, t); + if (a is EdgeInsetsDirectional && b is EdgeInsetsDirectional) + return EdgeInsetsDirectional.lerp(a, b, t); + return new _MixedEdgeInsets.fromLRSETB( + ui.lerpDouble(a._left, b._left, t), + ui.lerpDouble(a._right, b._right, t), + ui.lerpDouble(a._start, b._start, t), + ui.lerpDouble(a._end, b._end, t), + ui.lerpDouble(a._top, b._top, t), + ui.lerpDouble(a._bottom, b._bottom, t) + ); + } + + /// Convert this instance into a [EdgeInsets], which uses literal coordinates + /// (i.e. the `left` coordinate being explicitly a distance from the left, and + /// the `right` coordinate being explicitly a distance from the right). + /// + /// See also: + /// + /// * [EdgeInsets], for which this is a no-op (returns itself). + /// * [EdgeInsetsDirectional], which flips the horizontal direction + /// based on the `direction` argument. + EdgeInsets resolve(TextDirection direction); + + @override + String toString() { + if (_start == 0.0 && _end == 0.0) { + if (_left == 0.0 && _right == 0.0 && _top == 0.0 && _bottom == 0.0) + return 'EdgeInsets.zero'; + if (_left == _right && _right == _top && _top == _bottom) + return 'EdgeInsets.all(${_left.toStringAsFixed(1)})'; + return 'EdgeInsets(${_left.toStringAsFixed(1)}, ' + '${_top.toStringAsFixed(1)}, ' + '${_right.toStringAsFixed(1)}, ' + '${_bottom.toStringAsFixed(1)})'; + } + if (_left == 0.0 && _right == 0.0) { + return 'EdgeInsetsDirectional(${_start.toStringAsFixed(1)}, ' + '${_top.toStringAsFixed(1)}, ' + '${_end.toStringAsFixed(1)}, ' + '${_bottom.toStringAsFixed(1)})'; + } + return 'EdgeInsets(${_left.toStringAsFixed(1)}, ' + '${_top.toStringAsFixed(1)}, ' + '${_right.toStringAsFixed(1)}, ' + '${_bottom.toStringAsFixed(1)})' + ' + ' + 'EdgeInsetsDirectional(${_start.toStringAsFixed(1)}, ' + '0.0, ' + '${_end.toStringAsFixed(1)}, ' + '0.0)'; + } + + @override + bool operator ==(dynamic other) { + if (other is! EdgeInsetsGeometry) + return false; + final EdgeInsetsGeometry typedOther = other; + return _left == typedOther._left + && _right == typedOther._right + && _start == typedOther._start + && _end == typedOther._end + && _top == typedOther._top + && _bottom == typedOther._bottom; + } + + @override + int get hashCode => hashValues(_left, _right, _start, _end, _top, _bottom); } /// An immutable set of offsets in each of the four cardinal directions. @@ -46,13 +288,16 @@ enum Axis { /// /// See also: /// -/// * [Padding], a widget that describes margins using [EdgeInsets]. +/// * [Padding], a widget that accepts [EdgeInsets] to describe its margins. +/// * [EdgeInsetsDirectional], which (for properties and arguments that accept +/// the type [EdgeInsetsGeometry]) allows the horizontal insets to be +/// specified in a [TextDirection]-aware manner. @immutable -class EdgeInsets { +class EdgeInsets extends EdgeInsetsGeometry { /// Creates insets from offsets from the left, top, right, and bottom. const EdgeInsets.fromLTRB(this.left, this.top, this.right, this.bottom); - /// Creates insets where all the offsets are value. + /// Creates insets where all the offsets are `value`. /// /// ## Sample code /// @@ -108,15 +353,33 @@ class EdgeInsets { /// The offset from the left. final double left; + @override + double get _left => left; + /// The offset from the top. final double top; + @override + double get _top => top; + /// The offset from the right. final double right; + @override + double get _right => right; + /// The offset from the bottom. final double bottom; + @override + double get _bottom => bottom; + + @override + double get _start => 0.0; + + @override + double get _end => 0.0; + /// An Offset describing the vector from the top left of a rectangle to the /// top left of that rectangle inset by this object. Offset get topLeft => new Offset(left, top); @@ -133,31 +396,8 @@ class EdgeInsets { /// bottom right of that rectangle inset by this object. Offset get bottomRight => new Offset(-right, -bottom); - /// Whether every dimension is non-negative. - bool get isNonNegative => left >= 0.0 && top >= 0.0 && right >= 0.0 && bottom >= 0.0; - - /// The total offset in the vertical direction. - double get horizontal => left + right; - - /// The total offset in the horizontal direction. - double get vertical => top + bottom; - - /// The total offset in the given direction. - double along(Axis axis) { - assert(axis != null); - switch (axis) { - case Axis.horizontal: - return horizontal; - case Axis.vertical: - return vertical; - } - return null; - } - - /// The size that this EdgeInsets would occupy with an empty interior. - Size get collapsedSize => new Size(horizontal, vertical); - - /// An EdgeInsets with top and bottom as well as left and right flipped. + /// An [EdgeInsets] with top and bottom as well as left and right flipped. + @override EdgeInsets get flipped => new EdgeInsets.fromLTRB(right, bottom, left, top); /// Returns a new rect that is bigger than the given rect in each direction by @@ -191,92 +431,98 @@ class EdgeInsets { return new Rect.fromLTRB(rect.left + left, rect.top + top, rect.right - right, rect.bottom - bottom); } - /// Returns a new size that is bigger than the given size by the amount of - /// inset in the horizontal and vertical directions. - /// - /// See also: - /// - /// * [inflateRect], to inflate a [Rect] rather than a [Size]. - /// * [deflateSize], to deflate a [Size] rather than inflating it. - Size inflateSize(Size size) { - return new Size(size.width + horizontal, size.height + vertical); + @override + EdgeInsetsGeometry subtract(EdgeInsetsGeometry other) { + if (other is EdgeInsets) + return this - other; + return super.subtract(other); } - /// Returns a new size that is smaller than the given size by the amount of - /// inset in the horizontal and vertical directions. - /// - /// If the argument is smaller than [collapsedSize], then the resulting size - /// will have negative dimensions. - /// - /// See also: - /// - /// * [deflateRect], to deflate a [Rect] rather than a [Size]. - /// * [inflateSize], to inflate a [Size] rather than deflating it. - Size deflateSize(Size size) { - return new Size(size.width - horizontal, size.height - vertical); + @override + EdgeInsetsGeometry add(EdgeInsetsGeometry other) { + if (other is EdgeInsets) + return this + other; + return super.add(other); } - /// Returns the difference between two EdgeInsets. + /// Returns the difference between two [EdgeInsets]. EdgeInsets operator -(EdgeInsets other) { return new EdgeInsets.fromLTRB( left - other.left, top - other.top, right - other.right, - bottom - other.bottom + bottom - other.bottom, ); } - /// Returns the sum of two EdgeInsets. + /// Returns the sum of two [EdgeInsets]. EdgeInsets operator +(EdgeInsets other) { return new EdgeInsets.fromLTRB( left + other.left, top + other.top, right + other.right, - bottom + other.bottom + bottom + other.bottom, ); } - /// Scales the EdgeInsets in each dimension by the given factor. + /// Returns the [EdgeInsets] object with each dimension negated. + /// + /// This is the same as multiplying the object by -1.0. + @override + EdgeInsets operator -() { + return new EdgeInsets.fromLTRB( + -left, + -top, + -right, + -bottom, + ); + } + + /// Scales the [EdgeInsets] in each dimension by the given factor. + @override EdgeInsets operator *(double other) { return new EdgeInsets.fromLTRB( left * other, top * other, right * other, - bottom * other + bottom * other, ); } - /// Divides the EdgeInsets in each dimension by the given factor. + /// Divides the [EdgeInsets] in each dimension by the given factor. + @override EdgeInsets operator /(double other) { return new EdgeInsets.fromLTRB( left / other, top / other, right / other, - bottom / other + bottom / other, ); } - /// Integer divides the EdgeInsets in each dimension by the given factor. + /// Integer divides the [EdgeInsets] in each dimension by the given factor. + @override EdgeInsets operator ~/(double other) { return new EdgeInsets.fromLTRB( (left ~/ other).toDouble(), (top ~/ other).toDouble(), (right ~/ other).toDouble(), - (bottom ~/ other).toDouble() + (bottom ~/ other).toDouble(), ); } /// Computes the remainder in each dimension by the given factor. + @override EdgeInsets operator %(double other) { return new EdgeInsets.fromLTRB( left % other, top % other, right % other, - bottom % other + bottom % other, ); } - /// Linearly interpolate between two EdgeInsets. + /// Linearly interpolate between two [EdgeInsets]. /// /// If either is null, this function interpolates from [EdgeInsets.zero]. static EdgeInsets lerp(EdgeInsets a, EdgeInsets b, double t) { @@ -290,29 +536,331 @@ class EdgeInsets { ui.lerpDouble(a.left, b.left, t), ui.lerpDouble(a.top, b.top, t), ui.lerpDouble(a.right, b.right, t), - ui.lerpDouble(a.bottom, b.bottom, t) + ui.lerpDouble(a.bottom, b.bottom, t), ); } - /// An EdgeInsets with zero offsets in each direction. - static const EdgeInsets zero = const EdgeInsets.all(0.0); + /// An [EdgeInsets] with zero offsets in each direction. + static const EdgeInsets zero = const EdgeInsets.only(); @override - bool operator ==(dynamic other) { - if (identical(this, other)) - return true; - if (other is! EdgeInsets) - return false; - final EdgeInsets typedOther = other; - return left == typedOther.left && - top == typedOther.top && - right == typedOther.right && - bottom == typedOther.bottom; + EdgeInsets resolve(TextDirection direction) => this; +} + +/// An immutable set of offsets in each of the four cardinal directions, but +/// whose horizontal components are dependent on the writing direction. +/// +/// This can be used to indicate padding from the left in [TextDirection.ltr] +/// text and padding from the right in [TextDirection.rtl] text without having +/// to be aware of the current text direction. +/// +/// See also: +/// +/// * [EdgeInsets], a variant that uses physical labels (left and right instead +/// of start and end). +class EdgeInsetsDirectional extends EdgeInsetsGeometry { + /// Creates insets from offsets from the start, top, end, and bottom. + const EdgeInsetsDirectional.fromSTEB(this.start, this.top, this.end, this.bottom); + + /// Creates insets with only the given values non-zero. + /// + /// ## Sample code + /// + /// A margin indent of 40 pixels on the leading side: + /// + /// ```dart + /// const EdgeInsetsDirectional.only(start: 40.0) + /// ``` + const EdgeInsetsDirectional.only({ + this.start: 0.0, + this.top: 0.0, + this.end: 0.0, + this.bottom: 0.0 + }); + + /// The offset from the start side, the side from which the user will start + /// reading text. + /// + /// This value is normalized into an [EdgeInsets.left] or [EdgeInsets.right] + /// value by the [resolve] method. + final double start; + + @override + double get _start => start; + + /// The offset from the top. + /// + /// This value is passed through to [EdgeInsets.top] unmodified by the + /// [resolve] method. + final double top; + + @override + double get _top => top; + + /// The offset from the end side, the side on which the user ends reading + /// text. + /// + /// This value is normalized into an [EdgeInsets.left] or [EdgeInsets.right] + /// value by the [resolve] method. + final double end; + + @override + double get _end => end; + + /// The offset from the bottom. + /// + /// This value is passed through to [EdgeInsets.bottom] unmodified by the + /// [resolve] method. + final double bottom; + + @override + double get _bottom => bottom; + + @override + double get _left => 0.0; + + @override + double get _right => 0.0; + + @override + bool get isNonNegative => start >= 0.0 && top >= 0.0 && end >= 0.0 && bottom >= 0.0; + + /// An [EdgeInsetsDirectional] with [top] and [bottom] as well as [start] and [end] flipped. + @override + EdgeInsetsDirectional get flipped => new EdgeInsetsDirectional.fromSTEB(end, bottom, start, top); + + @override + EdgeInsetsGeometry subtract(EdgeInsetsGeometry other) { + if (other is EdgeInsetsDirectional) + return this - other; + return super.subtract(other); } @override - int get hashCode => hashValues(left, top, right, bottom); + EdgeInsetsGeometry add(EdgeInsetsGeometry other) { + if (other is EdgeInsetsDirectional) + return this + other; + return super.add(other); + } + + /// Returns the difference between two [EdgeInsetsDirectional] objects. + EdgeInsetsDirectional operator -(EdgeInsetsDirectional other) { + return new EdgeInsetsDirectional.fromSTEB( + start - other.start, + top - other.top, + end - other.end, + bottom - other.bottom, + ); + } + + /// Returns the sum of two [EdgeInsetsDirectional] objects. + EdgeInsetsDirectional operator +(EdgeInsetsDirectional other) { + return new EdgeInsetsDirectional.fromSTEB( + start + other.start, + top + other.top, + end + other.end, + bottom + other.bottom, + ); + } + + /// Returns the [EdgeInsetsDirectional] object with each dimension negated. + /// + /// This is the same as multiplying the object by -1.0. + @override + EdgeInsetsDirectional operator -() { + return new EdgeInsetsDirectional.fromSTEB( + -start, + -top, + -end, + -bottom, + ); + } + + /// Scales the [EdgeInsetsDirectional] object in each dimension by the given factor. + @override + EdgeInsetsDirectional operator *(double other) { + return new EdgeInsetsDirectional.fromSTEB( + start * other, + top * other, + end * other, + bottom * other, + ); + } + + /// Divides the [EdgeInsetsDirectional] object in each dimension by the given factor. + @override + EdgeInsetsDirectional operator /(double other) { + return new EdgeInsetsDirectional.fromSTEB( + start / other, + top / other, + end / other, + bottom / other, + ); + } + + /// Integer divides the [EdgeInsetsDirectional] object in each dimension by the given factor. + @override + EdgeInsetsDirectional operator ~/(double other) { + return new EdgeInsetsDirectional.fromSTEB( + (start ~/ other).toDouble(), + (top ~/ other).toDouble(), + (end ~/ other).toDouble(), + (bottom ~/ other).toDouble(), + ); + } + + /// Computes the remainder in each dimension by the given factor. + @override + EdgeInsetsDirectional operator %(double other) { + return new EdgeInsetsDirectional.fromSTEB( + start % other, + top % other, + end % other, + bottom % other, + ); + } + + /// Linearly interpolate between two [EdgeInsetsDirectional]. + /// + /// If either is null, this function interpolates from [EdgeInsetsDirectional.zero]. + /// + /// To interpolate between two [EdgeInsetsGeometry] objects of arbitrary type + /// (either [EdgeInsets] or [EdgeInsetsDirectional]), consider the + /// [EdgeInsetsGeometry.lerp] static method. + static EdgeInsetsDirectional lerp(EdgeInsetsDirectional a, EdgeInsetsDirectional b, double t) { + if (a == null && b == null) + return null; + if (a == null) + return b * t; + if (b == null) + return a * (1.0 - t); + return new EdgeInsetsDirectional.fromSTEB( + ui.lerpDouble(a.start, b.start, t), + ui.lerpDouble(a.top, b.top, t), + ui.lerpDouble(a.end, b.end, t), + ui.lerpDouble(a.bottom, b.bottom, t), + ); + } + + /// An [EdgeInsetsDirectional] with zero offsets in each direction. + /// + /// Consider using [EdgeInsets.zero] instead, since that object has the same + /// effect, but will be cheaper to [resolve]. + static const EdgeInsetsDirectional zero = const EdgeInsetsDirectional.only(); @override - String toString() => "EdgeInsets($left, $top, $right, $bottom)"; + EdgeInsets resolve(TextDirection direction) { + assert(direction != null); + switch (direction) { + case TextDirection.ltr: + return new EdgeInsets.fromLTRB(start, top, end, bottom); + case TextDirection.rtl: + return new EdgeInsets.fromLTRB(end, top, start, bottom); + } + return null; + } +} + +class _MixedEdgeInsets extends EdgeInsetsGeometry { + const _MixedEdgeInsets.fromLRSETB(this._left, this._right, this._start, this._end, this._top, this._bottom); + + @override + final double _left; + + @override + final double _right; + + @override + final double _start; + + @override + final double _end; + + @override + final double _top; + + @override + final double _bottom; + + @override + bool get isNonNegative { + return _left >= 0.0 + && _right >= 0.0 + && _start >= 0.0 + && _end >= 0.0 + && _top >= 0.0 + && _bottom >= 0.0; + } + + @override + _MixedEdgeInsets operator -() { + return new _MixedEdgeInsets.fromLRSETB( + -_left, + -_right, + -_start, + -_end, + -_top, + -_bottom, + ); + } + + @override + _MixedEdgeInsets operator *(double other) { + return new _MixedEdgeInsets.fromLRSETB( + _left * other, + _right * other, + _start * other, + _end * other, + _top * other, + _bottom * other, + ); + } + + @override + _MixedEdgeInsets operator /(double other) { + return new _MixedEdgeInsets.fromLRSETB( + _left / other, + _right / other, + _start / other, + _end / other, + _top / other, + _bottom / other, + ); + } + + @override + _MixedEdgeInsets operator ~/(double other) { + return new _MixedEdgeInsets.fromLRSETB( + (_left ~/ other).toDouble(), + (_right ~/ other).toDouble(), + (_start ~/ other).toDouble(), + (_end ~/ other).toDouble(), + (_top ~/ other).toDouble(), + (_bottom ~/ other).toDouble(), + ); + } + + @override + _MixedEdgeInsets operator %(double other) { + return new _MixedEdgeInsets.fromLRSETB( + _left % other, + _right % other, + _start % other, + _end % other, + _top % other, + _bottom % other, + ); + } + + @override + EdgeInsets resolve(TextDirection direction) { + assert(direction != null); + switch (direction) { + case TextDirection.ltr: + return new EdgeInsets.fromLTRB(_start + _left, _top, _end + _right, _bottom); + case TextDirection.rtl: + return new EdgeInsets.fromLTRB(_end + _left, _top, _start + _left, _bottom); + } + return null; + } } diff --git a/packages/flutter/lib/src/painting/fractional_offset.dart b/packages/flutter/lib/src/painting/fractional_offset.dart index 90b4549986..76c140d2ee 100644 --- a/packages/flutter/lib/src/painting/fractional_offset.dart +++ b/packages/flutter/lib/src/painting/fractional_offset.dart @@ -8,6 +8,212 @@ import 'package:flutter/foundation.dart'; import 'basic_types.dart'; +/// Base class for [FractionalOffset] that allows for text-direction aware +/// resolution. +/// +/// A property or argument of this type accepts classes created either with [new +/// FractionalOffset] and its variants, or [new FractionalOffsetDirectional]. +/// +/// To convert a [FractionalOffsetGeometry] object of indeterminate type into a +/// [FractionalOffset] object, call the [resolve] method. +abstract class FractionalOffsetGeometry { + /// Abstract const constructor. This constructor enables subclasses to provide + /// const constructors so that they can be used in const expressions. + const FractionalOffsetGeometry(); + + double get _dx; + double get _start; + double get _dy; + + /// Returns the difference between two [FractionalOffsetGeometry] objects. + /// + /// If you know you are applying this to two [FractionalOffset]s or two + /// [FractionalOffsetDirectional] objects, consider using the binary infix `-` + /// operator instead, which always returns an object of the same type as the + /// operands, and is typed accordingly. + /// + /// If [subtract] is applied to two objects of the same type ([FractionalOffset] or + /// [FractionalOffsetDirectional]), 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 [FractionalOffset] using [resolve]. + /// + /// This method returns the same result as [add] applied to the result of + /// negating the argument (using the prefix unary `-` operator or multiplying + /// the argument by -1.0 using the `*` operator). + FractionalOffsetGeometry subtract(FractionalOffsetGeometry other) { + return new _MixedFractionalOffset( + _dx - other._dx, + _start - other._start, + _dy - other._dy, + ); + } + + /// Returns the sum of two [FractionalOffsetGeometry] objects. + /// + /// If you know you are adding two [FractionalOffset] or two [FractionalOffsetDirectional] + /// 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 ([FractionalOffset] or + /// [FractionalOffsetDirectional]), 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 [FractionalOffset] using [resolve]. + FractionalOffsetGeometry add(FractionalOffsetGeometry other) { + return new _MixedFractionalOffset( + _dx + other._dx, + _start + other._start, + _dy + other._dy, + ); + } + + /// Returns the negation of the given [FractionalOffsetGeometry] object. + /// + /// This is the same as multiplying the object by -1.0. + /// + /// This operator returns an object of the same type as the operand. + FractionalOffsetGeometry operator -(); + + /// Scales the [FractionalOffsetGeometry] object in each dimension by the given factor. + /// + /// This operator returns an object of the same type as the operand. + FractionalOffsetGeometry operator *(double other); + + /// Divides the [FractionalOffsetGeometry] object in each dimension by the given factor. + /// + /// This operator returns an object of the same type as the operand. + FractionalOffsetGeometry operator /(double other); + + /// Integer divides the [FractionalOffsetGeometry] object in each dimension by the given factor. + /// + /// This operator returns an object of the same type as the operand. + FractionalOffsetGeometry 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. + FractionalOffsetGeometry operator %(double other); + + /// Linearly interpolate between two [FractionalOffsetGeometry] objects. + /// + /// If either is null, this function interpolates from [FractionalOffset.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 ([FractionalOffset] or + /// [FractionalOffsetDirectional]), 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 [FractionalOffset] using [resolve]. + static FractionalOffsetGeometry lerp(FractionalOffsetGeometry a, FractionalOffsetGeometry b, double t) { + if (a == null && b == null) + return null; + if ((a == null || a is FractionalOffset) && (b == null || b is FractionalOffset)) + return FractionalOffset.lerp(a, b, t); + if ((a == null || a is FractionalOffsetDirectional) && (b == null || b is FractionalOffsetDirectional)) + return FractionalOffsetDirectional.lerp(a, b, t); + if (a == null) { + return new _MixedFractionalOffset( + ui.lerpDouble(0.5, b._dx, t), + ui.lerpDouble(0.0, b._start, t), + ui.lerpDouble(0.5, b._dy, t), + ); + } + if (b == null) { + return new _MixedFractionalOffset( + ui.lerpDouble(a._dx, 0.5, t), + ui.lerpDouble(a._start, 0.0, t), + ui.lerpDouble(a._dy, 0.5, t), + ); + } + return new _MixedFractionalOffset( + ui.lerpDouble(a._dx, b._dx, t), + ui.lerpDouble(a._start, b._start, t), + ui.lerpDouble(a._dy, b._dy, t), + ); + } + + /// Convert this instance into a [FractionalOffset], which uses literal + /// coordinates (the `x` coordinate being explicitly a distance from the + /// left). + /// + /// See also: + /// + /// * [FractionalOffset], for which this is a no-op (returns itself). + /// * [FractionalOffsetDirectional], which flips the horizontal direction + /// based on the `direction` argument. + FractionalOffset resolve(TextDirection direction); + + @override + String toString() { + double x = _dx; + double start = _start; + if (this is FractionalOffset) { + assert(start == 0.0); + start = null; + } else if (start == 0.5) { + x += 0.5; + start = null; + } + if (start == null) { + if (x == 0.0 && _dy == 0.0) + return 'FractionalOffset.topLeft'; + if (x == 0.5 && _dy == 0.0) + return 'FractionalOffset.topCenter'; + if (x == 1.0 && _dy == 0.0) + return 'FractionalOffset.topRight'; + if (x == 0.0 && _dy == 0.5) + return 'FractionalOffset.centerLeft'; + if (x == 0.5 && _dy == 0.5) + return 'FractionalOffset.center'; + if (x == 1.0 && _dy == 0.5) + return 'FractionalOffset.centerRight'; + if (x == 0.0 && _dy == 1.0) + return 'FractionalOffset.bottomLeft'; + if (x == 0.5 && _dy == 1.0) + return 'FractionalOffset.bottomCenter'; + if (x == 1.0 && _dy == 1.0) + return 'FractionalOffset.bottomRight'; + return 'FractionalOffset(${x.toStringAsFixed(1)}, ' + '${_dy.toStringAsFixed(1)})'; + } else if (x == 0.0) { + assert(start != 0.5); + if (start == 0.0 && _dy == 0.0) + return 'FractionalOffsetDirectional.topStart'; + if (start == 1.0 && _dy == 0.0) + return 'FractionalOffsetDirectional.topEnd'; + if (start == 0.0 && _dy == 0.5) + return 'FractionalOffsetDirectional.centerStart'; + if (start == 1.0 && _dy == 0.5) + return 'FractionalOffsetDirectional.centerEnd'; + if (start == 0.0 && _dy == 1.0) + return 'FractionalOffsetDirectional.bottomStart'; + if (start == 1.0 && _dy == 1.0) + return 'FractionalOffsetDirectional.bottomEnd'; + return 'FractionalOffsetDirectional(${start.toStringAsFixed(1)}, ' + '${_dy.toStringAsFixed(1)})'; + } + return 'FractionalOffset(${_dx.toStringAsFixed(1)}, ' + '${_dy.toStringAsFixed(1)})' + ' + ' + 'FractionalOffsetDirectional(${_start.toStringAsFixed(1)}, ' + '0.0)'; + } + + @override + bool operator ==(dynamic other) { + if (other is! FractionalOffsetGeometry) + return false; + final FractionalOffsetGeometry typedOther = other; + return _dx == typedOther._dx && + _start == typedOther._start && + _dy == typedOther._dy; + } + + @override + int get hashCode => hashValues(_dx, _start, _dy); +} + /// An offset that's expressed as a fraction of a [Size]. /// /// `FractionalOffset(1.0, 0.0)` represents the top right of the [Size]. @@ -22,8 +228,14 @@ import 'basic_types.dart'; /// /// * [Align] positions a child according to a [FractionalOffset]. /// * [FractionalTranslation] moves a child according to a [FractionalOffset]. +/// +/// See also: +/// +/// * [FractionalOffsetDirectional], which (for properties and arguments that +/// accept the type [FractionalOffsetGeometry]) allows the horizontal +/// coordinate to be specified in a [TextDirection]-aware manner. @immutable -class FractionalOffset { +class FractionalOffset extends FractionalOffsetGeometry { /// Creates a fractional offset. /// /// The [dx] and [dy] arguments must not be null. @@ -66,14 +278,23 @@ class FractionalOffset { /// edge. final double dx; + @override + double get _dx => dx; + /// The distance fraction in the vertical direction. /// /// A value of 0.0 corresponds to the topmost edge. A value of 1.0 corresponds /// to the bottommost edge. Values are not limited to that range; negative - /// values represent positions above the top, and values greated than 1.0 + /// values represent positions above the top, and values greater than 1.0 /// represent positions below the bottom. final double dy; + @override + double get _dy => dy; + + @override + double get _start => 0.0; + /// The top left corner. static const FractionalOffset topLeft = const FractionalOffset(0.0, 0.0); @@ -83,6 +304,15 @@ class FractionalOffset { /// The top right corner. static const FractionalOffset topRight = const FractionalOffset(1.0, 0.0); + /// The center point along the left edge. + static const FractionalOffset centerLeft = const FractionalOffset(0.0, 0.5); + + /// The center point, both horizontally and vertically. + static const FractionalOffset center = const FractionalOffset(0.5, 0.5); + + /// The center point along the right edge. + static const FractionalOffset centerRight = const FractionalOffset(1.0, 0.5); + /// The bottom left corner. static const FractionalOffset bottomLeft = const FractionalOffset(0.0, 1.0); @@ -92,46 +322,56 @@ class FractionalOffset { /// The bottom right corner. static const FractionalOffset bottomRight = const FractionalOffset(1.0, 1.0); - /// The center point along the left edge. - static const FractionalOffset centerLeft = const FractionalOffset(0.0, 0.5); - - /// The center point along the right edge. - static const FractionalOffset centerRight = const FractionalOffset(1.0, 0.5); - - /// The center point, both horizontally and vertically. - static const FractionalOffset center = const FractionalOffset(0.5, 0.5); - - /// Returns the negation of the given fractional offset. - FractionalOffset operator -() { - return new FractionalOffset(-dx, -dy); + @override + FractionalOffsetGeometry subtract(FractionalOffsetGeometry other) { + if (other is FractionalOffset) + return this - other; + return super.subtract(other); } - /// Returns the difference between two fractional offsets. + @override + FractionalOffsetGeometry add(FractionalOffsetGeometry other) { + if (other is FractionalOffset) + return this + other; + return super.add(other); + } + + /// Returns the difference between two [FractionalOffset]s. FractionalOffset operator -(FractionalOffset other) { return new FractionalOffset(dx - other.dx, dy - other.dy); } - /// Returns the sum of two fractional offsets. + /// Returns the sum of two [FractionalOffset]s. FractionalOffset operator +(FractionalOffset other) { return new FractionalOffset(dx + other.dx, dy + other.dy); } - /// Scales the fractional offset in each dimension by the given factor. + /// Returns the negation of the given [FractionalOffset]. + @override + FractionalOffset operator -() { + return new FractionalOffset(-dx, -dy); + } + + /// Scales the [FractionalOffset] in each dimension by the given factor. + @override FractionalOffset operator *(double other) { return new FractionalOffset(dx * other, dy * other); } - /// Divides the fractional offset in each dimension by the given factor. + /// Divides the [FractionalOffset] in each dimension by the given factor. + @override FractionalOffset operator /(double other) { return new FractionalOffset(dx / other, dy / other); } - /// Integer divides the fractional offset in each dimension by the given factor. + /// Integer divides the [FractionalOffset] in each dimension by the given factor. + @override FractionalOffset operator ~/(double other) { return new FractionalOffset((dx ~/ other).toDouble(), (dy ~/ other).toDouble()); } /// Computes the remainder in each dimension by the given factor. + @override FractionalOffset operator %(double other) { return new FractionalOffset(dx % other, dy % other); } @@ -161,37 +401,261 @@ class FractionalOffset { rect.left + (rect.width - size.width) * dx, rect.top + (rect.height - size.height) * dy, size.width, - size.height + size.height, ); } - @override - bool operator ==(dynamic other) { - if (other is! FractionalOffset) - return false; - final FractionalOffset typedOther = other; - return dx == typedOther.dx && - dy == typedOther.dy; - } - - @override - int get hashCode => hashValues(dx, dy); - - /// Linearly interpolate between two EdgeInsets. + /// Linearly interpolate between two [FractionalOffset]s. /// - /// If either is null, this function interpolates from [FractionalOffset.topLeft]. - // TODO(abarth): Consider interpolating from [FractionalOffset.center] instead - // to remove upper-left bias. + /// If either is null, this function interpolates from [FractionalOffset.center]. static FractionalOffset lerp(FractionalOffset a, FractionalOffset b, double t) { if (a == null && b == null) return null; if (a == null) - return new FractionalOffset(b.dx * t, b.dy * t); + return new FractionalOffset(ui.lerpDouble(0.5, b.dx, t), ui.lerpDouble(0.5, b.dy, t)); if (b == null) - return new FractionalOffset(a.dx * (1.0 - t), a.dy * (1.0 - t)); + return new FractionalOffset(ui.lerpDouble(a.dx, 0.5, t), ui.lerpDouble(a.dy, 0.5, t)); return new FractionalOffset(ui.lerpDouble(a.dx, b.dx, t), ui.lerpDouble(a.dy, b.dy, t)); } @override - String toString() => '$runtimeType($dx, $dy)'; + FractionalOffset resolve(TextDirection direction) => this; +} + +/// 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: +/// +/// * [FractionalOffset], a variant that is defined in physical terms (i.e. +/// whose horizontal component does not depend on the text direction). +class FractionalOffsetDirectional extends FractionalOffsetGeometry { + /// Creates a directional fractional offset. + /// + /// The [start] and [dy] arguments must not be null. + const FractionalOffsetDirectional(this.start, this.dy) + : assert(start != null), + assert(dy != null); + + /// The distance fraction in the horizontal direction. + /// + /// A value of 0.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; negative + /// values represent positions beyond the start edge, and values greater than + /// 1.0 represent positions beyond the end edge. + /// + /// This value is normalized into a [FractionalOffset.dx] value by the + /// [resolve] method. + final double start; + + @override + double get _start => start; + + /// The distance fraction in the vertical direction. + /// + /// A value of 0.0 corresponds to the topmost edge. A value of 1.0 corresponds + /// to the bottommost edge. Values are not limited to that range; negative + /// values represent positions above the top, and values greater than 1.0 + /// represent positions below the bottom. + /// + /// This value is passed through to [FractionalOffset.dy] unmodified by the + /// [resolve] method. + final double dy; + + @override + double get _dy => dy; + + @override + double get _dx => 0.0; + + /// The top corner on the "start" side. + static const FractionalOffsetDirectional topStart = const FractionalOffsetDirectional(0.0, 0.0); + + /// The center point along the top edge. + /// + /// Consider using [FractionalOffset.topCenter] instead, as it does not need + /// to be [resolve]d to be used. + static const FractionalOffsetDirectional topCenter = const FractionalOffsetDirectional(0.5, 0.0); + + /// The top corner on the "end" side. + static const FractionalOffsetDirectional topEnd = const FractionalOffsetDirectional(1.0, 0.0); + + /// The center point along the "start" edge. + static const FractionalOffsetDirectional centerStart = const FractionalOffsetDirectional(0.0, 0.5); + + /// The center point, both horizontally and vertically. + /// + /// Consider using [FractionalOffset.center] instead, as it does not need to + /// be [resolve]d to be used. + static const FractionalOffsetDirectional center = const FractionalOffsetDirectional(0.5, 0.5); + + /// The center point along the "end" edge. + static const FractionalOffsetDirectional centerEnd = const FractionalOffsetDirectional(1.0, 0.5); + + /// The bottom corner on the "start" side. + static const FractionalOffsetDirectional bottomStart = const FractionalOffsetDirectional(0.0, 1.0); + + /// The center point along the bottom edge. + /// + /// Consider using [FractionalOffset.bottomCenter] instead, as it does not + /// need to be [resolve]d to be used. + static const FractionalOffsetDirectional bottomCenter = const FractionalOffsetDirectional(0.5, 1.0); + + /// The bottom corner on the "end" side. + static const FractionalOffsetDirectional bottomEnd = const FractionalOffsetDirectional(1.0, 1.0); + + @override + FractionalOffsetGeometry subtract(FractionalOffsetGeometry other) { + if (other is FractionalOffsetDirectional) + return this - other; + return super.subtract(other); + } + + @override + FractionalOffsetGeometry add(FractionalOffsetGeometry other) { + if (other is FractionalOffsetDirectional) + return this + other; + return super.add(other); + } + + /// Returns the difference between two [FractionalOffsetDirectional]s. + FractionalOffsetDirectional operator -(FractionalOffsetDirectional other) { + return new FractionalOffsetDirectional(start - other.start, dy - other.dy); + } + + /// Returns the sum of two [FractionalOffsetDirectional]s. + FractionalOffsetDirectional operator +(FractionalOffsetDirectional other) { + return new FractionalOffsetDirectional(start + other.start, dy + other.dy); + } + + /// Returns the negation of the given [FractionalOffsetDirectional]. + @override + FractionalOffsetDirectional operator -() { + return new FractionalOffsetDirectional(-start, -dy); + } + + /// Scales the [FractionalOffsetDirectional] in each dimension by the given factor. + @override + FractionalOffsetDirectional operator *(double other) { + return new FractionalOffsetDirectional(start * other, dy * other); + } + + /// Divides the [FractionalOffsetDirectional] in each dimension by the given factor. + @override + FractionalOffsetDirectional operator /(double other) { + return new FractionalOffsetDirectional(start / other, dy / other); + } + + /// Integer divides the [FractionalOffsetDirectional] in each dimension by the given factor. + @override + FractionalOffsetDirectional operator ~/(double other) { + return new FractionalOffsetDirectional((start ~/ other).toDouble(), (dy ~/ other).toDouble()); + } + + /// Computes the remainder in each dimension by the given factor. + @override + FractionalOffsetDirectional operator %(double other) { + return new FractionalOffsetDirectional(start % other, dy % other); + } + + /// Linearly interpolate between two [FractionalOffsetDirectional]s. + /// + /// If either is null, this function interpolates from [FractionalOffset.center]. + static FractionalOffsetDirectional lerp(FractionalOffsetDirectional a, FractionalOffsetDirectional b, double t) { + if (a == null && b == null) + return null; + if (a == null) + return new FractionalOffsetDirectional(ui.lerpDouble(0.5, b.start, t), ui.lerpDouble(0.5, b.dy, t)); + if (b == null) + return new FractionalOffsetDirectional(ui.lerpDouble(a.start, 0.5, t), ui.lerpDouble(a.dy, 0.5, t)); + return new FractionalOffsetDirectional(ui.lerpDouble(a.start, b.start, t), ui.lerpDouble(a.dy, b.dy, t)); + } + + @override + FractionalOffset resolve(TextDirection direction) { + assert(direction != null); + switch (direction) { + case TextDirection.ltr: + return new FractionalOffset(start, dy); + case TextDirection.rtl: + return new FractionalOffset(1.0 - start, dy); + } + return null; + } +} + +class _MixedFractionalOffset extends FractionalOffsetGeometry { + const _MixedFractionalOffset(this._dx, this._start, this._dy); + + @override + final double _dx; + + @override + final double _start; + + @override + final double _dy; + + @override + _MixedFractionalOffset operator -() { + return new _MixedFractionalOffset( + -_dx, + -_start, + -_dy, + ); + } + + @override + _MixedFractionalOffset operator *(double other) { + return new _MixedFractionalOffset( + _dx * other, + _start * other, + _dy * other, + ); + } + + @override + _MixedFractionalOffset operator /(double other) { + return new _MixedFractionalOffset( + _dx / other, + _start / other, + _dy / other, + ); + } + + @override + _MixedFractionalOffset operator ~/(double other) { + return new _MixedFractionalOffset( + (_dx ~/ other).toDouble(), + (_start ~/ other).toDouble(), + (_dy ~/ other).toDouble(), + ); + } + + @override + _MixedFractionalOffset operator %(double other) { + return new _MixedFractionalOffset( + _dx % other, + _start % other, + _dy % other, + ); + } + + @override + FractionalOffset resolve(TextDirection direction) { + assert(direction != null); + switch (direction) { + case TextDirection.ltr: + return new FractionalOffset(_start + _dx, _dy); + case TextDirection.rtl: + return new FractionalOffset((1.0 - _start) + _dx, _dy); + } + return null; + } } diff --git a/packages/flutter/lib/src/rendering/box.dart b/packages/flutter/lib/src/rendering/box.dart index 3cbad71d76..30f8f40208 100644 --- a/packages/flutter/lib/src/rendering/box.dart +++ b/packages/flutter/lib/src/rendering/box.dart @@ -2149,6 +2149,11 @@ abstract class RenderBoxContainerDefaultsMixin, RenderBoxContainerDefaultsMixin { /// Creates a flex render object. @@ -209,7 +270,9 @@ class RenderFlex extends RenderBox with ContainerRenderObjectMixin _mainAxisAlignment; MainAxisAlignment _mainAxisAlignment; set mainAxisAlignment(MainAxisAlignment value) { @@ -265,6 +338,14 @@ class RenderFlex extends RenderBox with ContainerRenderObjectMixin _crossAxisAlignment; CrossAxisAlignment _crossAxisAlignment; set crossAxisAlignment(CrossAxisAlignment value) { @@ -275,6 +356,62 @@ class RenderFlex extends RenderBox with ContainerRenderObjectMixin _textDirection; + TextDirection _textDirection; + set textDirection(TextDirection value) { + if (_textDirection != value) { + _textDirection = value; + markNeedsLayout(); + } + } + + /// Determines the order to lay children out vertically and how to interpret + /// `start` and `end` in the vertical direction. + /// + /// If the [direction] is [Axis.vertical], this controls which order children + /// are painted in (down or up), the meaning of the [mainAxisAlignment] + /// property's [MainAxisAlignment.start] and [MainAxisAlignment.end] values. + /// + /// If the [direction] is [Axis.vertical], and either the [mainAxisAlignment] + /// is either [MainAxisAlignment.start] or [MainAxisAlignment.end], or there's + /// more than one child, then the [verticalDirection] must not be null. + /// + /// If the [direction] is [Axis.horizontal], this controls the meaning of the + /// [crossAxisAlignment] property's [CrossAxisAlignment.start] and + /// [CrossAxisAlignment.end] values. + /// + /// If the [direction] is [Axis.horizontal], and the [crossAxisAlignment] is + /// either [CrossAxisAlignment.start] or [CrossAxisAlignment.end], then the + /// [verticalDirection] must not be null. + VerticalDirection get verticalDirection => _verticalDirection; + VerticalDirection _verticalDirection; + set verticalDirection(VerticalDirection value) { + if (_verticalDirection != value) { + _verticalDirection = value; + markNeedsLayout(); + } + } + /// If aligning items according to their baseline, which baseline to use. /// /// Must not be null if [crossAxisAlignment] is [CrossAxisAlignment.baseline]. @@ -288,6 +425,45 @@ class RenderFlex extends RenderBox with ContainerRenderObjectMixin('direction', _direction)); - description.add(new EnumProperty('mainAxisAlignment', _mainAxisAlignment)); - description.add(new EnumProperty('mainAxisSize', _mainAxisSize)); - description.add(new EnumProperty('crossAxisAlignment', _crossAxisAlignment)); - description.add(new EnumProperty('textBaseline', _textBaseline)); + description.add(new EnumProperty('direction', direction)); + description.add(new EnumProperty('mainAxisAlignment', mainAxisAlignment)); + description.add(new EnumProperty('mainAxisSize', mainAxisSize)); + description.add(new EnumProperty('crossAxisAlignment', crossAxisAlignment)); + description.add(new EnumProperty('textDirection', textDirection, defaultValue: null)); + description.add(new EnumProperty('verticalDirection', verticalDirection, defaultValue: null)); + description.add(new EnumProperty('textBaseline', textBaseline, defaultValue: null)); } } diff --git a/packages/flutter/lib/src/rendering/node.dart b/packages/flutter/lib/src/rendering/node.dart index a03c964a38..f7d7d4128e 100644 --- a/packages/flutter/lib/src/rendering/node.dart +++ b/packages/flutter/lib/src/rendering/node.dart @@ -43,7 +43,7 @@ class AbstractNode { int get depth => _depth; int _depth = 0; - /// Adjust the [depth] of the given [child] to be greated than this node's own + /// Adjust the [depth] of the given [child] to be greater than this node's own /// [depth]. /// /// Only call this method from overrides of [redepthChildren]. diff --git a/packages/flutter/lib/src/rendering/shifted_box.dart b/packages/flutter/lib/src/rendering/shifted_box.dart index 6a18f1bcf7..8be09e0bad 100644 --- a/packages/flutter/lib/src/rendering/shifted_box.dart +++ b/packages/flutter/lib/src/rendering/shifted_box.dart @@ -91,29 +91,58 @@ class RenderPadding extends RenderShiftedBox { /// /// The [padding] argument must not be null and must have non-negative insets. RenderPadding({ - @required EdgeInsets padding, - RenderBox child + @required EdgeInsetsGeometry padding, + TextDirection textDirection, + RenderBox child, }) : assert(padding != null), assert(padding.isNonNegative), + _textDirection = textDirection, _padding = padding, - super(child); + super(child) { + _applyUpdate(); + } + + // The resolved absolute insets. + EdgeInsets _resolvedPadding; + + void _applyUpdate() { + final EdgeInsets resolvedPadding = padding.resolve(textDirection); + assert(resolvedPadding.isNonNegative); + if (resolvedPadding != _resolvedPadding) { + _resolvedPadding = resolvedPadding; + markNeedsLayout(); + } + } /// The amount to pad the child in each dimension. - EdgeInsets get padding => _padding; - EdgeInsets _padding; - set padding(EdgeInsets value) { + /// + /// If this is set to an [EdgeInsetsDirectional] object, then [textDirection] + /// must be non-null. + EdgeInsetsGeometry get padding => _padding; + EdgeInsetsGeometry _padding; + set padding(EdgeInsetsGeometry value) { assert(value != null); assert(value.isNonNegative); if (_padding == value) return; _padding = value; - markNeedsLayout(); + _applyUpdate(); + } + + /// The text direction with which to resolve [padding]. + TextDirection get textDirection => _textDirection; + TextDirection _textDirection; + set textDirection(TextDirection value) { + if (_textDirection == value) + return; + _textDirection = value; + _applyUpdate(); } @override double computeMinIntrinsicWidth(double height) { - final double totalHorizontalPadding = padding.left + padding.right; - final double totalVerticalPadding = padding.top + padding.bottom; + final double totalHorizontalPadding = _resolvedPadding.left + _resolvedPadding.right; + final double totalVerticalPadding = _resolvedPadding.top + _resolvedPadding.bottom; if (child != null) // next line relies on double.INFINITY absorption return child.getMinIntrinsicWidth(math.max(0.0, height - totalVerticalPadding)) + totalHorizontalPadding; return totalHorizontalPadding; @@ -121,8 +150,8 @@ class RenderPadding extends RenderShiftedBox { @override double computeMaxIntrinsicWidth(double height) { - final double totalHorizontalPadding = padding.left + padding.right; - final double totalVerticalPadding = padding.top + padding.bottom; + final double totalHorizontalPadding = _resolvedPadding.left + _resolvedPadding.right; + final double totalVerticalPadding = _resolvedPadding.top + _resolvedPadding.bottom; if (child != null) // next line relies on double.INFINITY absorption return child.getMaxIntrinsicWidth(math.max(0.0, height - totalVerticalPadding)) + totalHorizontalPadding; return totalHorizontalPadding; @@ -130,8 +159,8 @@ class RenderPadding extends RenderShiftedBox { @override double computeMinIntrinsicHeight(double width) { - final double totalHorizontalPadding = padding.left + padding.right; - final double totalVerticalPadding = padding.top + padding.bottom; + final double totalHorizontalPadding = _resolvedPadding.left + _resolvedPadding.right; + final double totalVerticalPadding = _resolvedPadding.top + _resolvedPadding.bottom; if (child != null) // next line relies on double.INFINITY absorption return child.getMinIntrinsicHeight(math.max(0.0, width - totalHorizontalPadding)) + totalVerticalPadding; return totalVerticalPadding; @@ -139,8 +168,8 @@ class RenderPadding extends RenderShiftedBox { @override double computeMaxIntrinsicHeight(double width) { - final double totalHorizontalPadding = padding.left + padding.right; - final double totalVerticalPadding = padding.top + padding.bottom; + final double totalHorizontalPadding = _resolvedPadding.left + _resolvedPadding.right; + final double totalVerticalPadding = _resolvedPadding.top + _resolvedPadding.bottom; if (child != null) // next line relies on double.INFINITY absorption return child.getMaxIntrinsicHeight(math.max(0.0, width - totalHorizontalPadding)) + totalVerticalPadding; return totalVerticalPadding; @@ -148,21 +177,21 @@ class RenderPadding extends RenderShiftedBox { @override void performLayout() { - assert(padding != null); + assert(_resolvedPadding != null); if (child == null) { size = constraints.constrain(new Size( - padding.left + padding.right, - padding.top + padding.bottom + _resolvedPadding.left + _resolvedPadding.right, + _resolvedPadding.top + _resolvedPadding.bottom )); return; } - final BoxConstraints innerConstraints = constraints.deflate(padding); + final BoxConstraints innerConstraints = constraints.deflate(_resolvedPadding); child.layout(innerConstraints, parentUsesSize: true); final BoxParentData childParentData = child.parentData; - childParentData.offset = new Offset(padding.left, padding.top); + childParentData.offset = new Offset(_resolvedPadding.left, _resolvedPadding.top); size = constraints.constrain(new Size( - padding.left + child.size.width + padding.right, - padding.top + child.size.height + padding.bottom + _resolvedPadding.left + child.size.width + _resolvedPadding.right, + _resolvedPadding.top + child.size.height + _resolvedPadding.bottom )); } @@ -171,7 +200,7 @@ class RenderPadding extends RenderShiftedBox { super.debugPaintSize(context, offset); assert(() { final Rect outerRect = offset & size; - debugPaintPadding(context.canvas, outerRect, child != null ? padding.deflateRect(outerRect) : null); + debugPaintPadding(context.canvas, outerRect, child != null ? _resolvedPadding.deflateRect(outerRect) : null); return true; }); } @@ -179,7 +208,8 @@ class RenderPadding extends RenderShiftedBox { @override void debugFillProperties(DiagnosticPropertiesBuilder description) { super.debugFillProperties(description); - description.add(new DiagnosticsProperty('padding', padding)); + description.add(new DiagnosticsProperty('padding', padding)); + description.add(new EnumProperty('textDirection', textDirection)); } } diff --git a/packages/flutter/lib/src/rendering/sliver.dart b/packages/flutter/lib/src/rendering/sliver.dart index a1dee0ba25..4889d8a6b8 100644 --- a/packages/flutter/lib/src/rendering/sliver.dart +++ b/packages/flutter/lib/src/rendering/sliver.dart @@ -94,6 +94,10 @@ Axis axisDirectionToAxis(AxisDirection axisDirection) { /// Specifically, returns [AxisDirection.up] for [AxisDirection.down] (and /// vice versa), as well as [AxisDirection.left] for [AxisDirection.right] (and /// vice versa). +/// +/// See also: +/// +/// * [flipAxis], which does the same thing for [Axis] values. AxisDirection flipAxisDirection(AxisDirection axisDirection) { assert(axisDirection != null); switch (axisDirection) { diff --git a/packages/flutter/lib/src/widgets/app.dart b/packages/flutter/lib/src/widgets/app.dart index 2ef0d55bd0..29962da064 100644 --- a/packages/flutter/lib/src/widgets/app.dart +++ b/packages/flutter/lib/src/widgets/app.dart @@ -23,6 +23,19 @@ import 'widget_inspector.dart'; export 'dart:ui' show Locale; +// Delegate that fetches the default (English) strings. +class _WidgetsLocalizationsDelegate extends LocalizationsDelegate { + const _WidgetsLocalizationsDelegate(); + + @override + Future load(Locale locale) { + return new SynchronousFuture(const WidgetsLocalizations()); + } + + @override + bool shouldReload(_WidgetsLocalizationsDelegate old) => false; +} + /// A convenience class that wraps a number of widgets that are commonly /// required for an application. /// @@ -269,6 +282,14 @@ class _WidgetsAppState extends State implements WidgetsBindingObserv } } + // Combine the Localizations for Widgets with the ones contributed + // by the localizationsDelegates parameter, if any. + Iterable> get _localizationsDelegates sync* { + yield const _WidgetsLocalizationsDelegate(); // TODO(ianh): make this configurable + if (widget.localizationsDelegates != null) + yield* widget.localizationsDelegates; + } + @override void didChangeAppLifecycleState(AppLifecycleState state) { } @@ -281,7 +302,7 @@ class _WidgetsAppState extends State implements WidgetsBindingObserv data: new MediaQueryData.fromWindow(ui.window), child: new Localizations( locale: widget.locale ?? _locale, - delegates: widget.localizationsDelegates, + delegates: _localizationsDelegates.toList(), child: new Title( title: widget.title, color: widget.color, diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart index 27c1866787..76c824195f 100644 --- a/packages/flutter/lib/src/widgets/basic.dart +++ b/packages/flutter/lib/src/widgets/basic.dart @@ -1116,20 +1116,27 @@ class Padding extends SingleChildRenderObjectWidget { super(key: key, child: child); /// The amount of space by which to inset the child. - final EdgeInsets padding; + final EdgeInsetsGeometry padding; @override - RenderPadding createRenderObject(BuildContext context) => new RenderPadding(padding: padding); + RenderPadding createRenderObject(BuildContext context) { + return new RenderPadding( + padding: padding, + textDirection: Directionality.of(context), + ); + } @override void updateRenderObject(BuildContext context, RenderPadding renderObject) { - renderObject.padding = padding; + renderObject + ..padding = padding + ..textDirection = Directionality.of(context); } @override void debugFillProperties(DiagnosticPropertiesBuilder description) { super.debugFillProperties(description); - description.add(new DiagnosticsProperty('padding', padding)); + description.add(new DiagnosticsProperty('padding', padding)); } } @@ -2058,6 +2065,8 @@ class SliverPadding extends SingleChildRenderObjectWidget { /// The amount of space by which to inset the child sliver. final EdgeInsets padding; + // TODO(ianh): RTL + @override RenderSliverPadding createRenderObject(BuildContext context) => new RenderSliverPadding(padding: padding); @@ -2495,22 +2504,31 @@ class Flex extends MultiChildRenderObjectWidget { /// /// The [direction] is required. /// - /// The [direction], [mainAxisAlignment], and [crossAxisAlignment] arguments - /// must not be null. If [crossAxisAlignment] is + /// The [direction], [mainAxisAlignment], [crossAxisAlignment], and + /// [verticalDirection] arguments must not be null. If [crossAxisAlignment] is /// [CrossAxisAlignment.baseline], then [textBaseline] must not be null. + /// + /// The [textDirection] argument defaults to the ambient [Directionality], if + /// any. If there is no ambient directionality, and a text direction is going + /// to be necessary to decide which direction to lay the children in or to + /// disambiguate `start` or `end` values for the main or cross axis + /// directions, the [textDirection] must not be null. Flex({ Key key, @required this.direction, this.mainAxisAlignment: MainAxisAlignment.start, this.mainAxisSize: MainAxisSize.max, this.crossAxisAlignment: CrossAxisAlignment.center, + this.textDirection, + this.verticalDirection: VerticalDirection.down, this.textBaseline, List children: const [], }) : assert(direction != null), assert(mainAxisAlignment != null), assert(mainAxisSize != null), assert(crossAxisAlignment != null), - assert(crossAxisAlignment != CrossAxisAlignment.baseline || textBaseline != null),// https://github.com/dart-lang/sdk/issues/29278 + assert(verticalDirection != null), + assert(crossAxisAlignment != CrossAxisAlignment.baseline || textBaseline != null), super(key: key, children: children); /// The direction to use as the main axis. @@ -2546,9 +2564,88 @@ class Flex extends MultiChildRenderObjectWidget { /// children in the cross axis (e.g., horizontally for a [Column]). final CrossAxisAlignment crossAxisAlignment; + /// Determines the order to lay children out horizontally and how to interpret + /// `start` and `end` in the horizontal direction. + /// + /// Defaults to the ambient [Directionality]. + /// + /// If the [direction] is [Axis.horizontal], this controls which order + /// children are painted in (left-to-right or right-to-left), and the meaning + /// of the [mainAxisAlignment] property's [MainAxisAlignment.start] and + /// [MainAxisAlignment.end] values. + /// + /// If the [direction] is [Axis.horizontal], and either the + /// [mainAxisAlignment] is either [MainAxisAlignment.start] or + /// [MainAxisAlignment.end], or there's more than one child, then the + /// [textDirection] (or the ambient [Directionality]) must not be null. + /// + /// If the [direction] is [Axis.vertical], this controls the meaning of the + /// [crossAxisAlignment] property's [CrossAxisAlignment.start] and + /// [CrossAxisAlignment.end] values. + /// + /// If the [direction] is [Axis.vertical], and the [crossAxisAlignment] is + /// either [CrossAxisAlignment.start] or [CrossAxisAlignment.end], then the + /// [textDirection] (or the ambient [Directionality]) must not be null. + final TextDirection textDirection; + + /// Determines the order to lay children out vertically and how to interpret + /// `start` and `end` in the vertical direction. + /// + /// Defaults to [VerticalDirection.down]. + /// + /// If the [direction] is [Axis.vertical], this controls which order children + /// are painted in (down or up), the meaning of the [mainAxisAlignment] + /// property's [MainAxisAlignment.start] and [MainAxisAlignment.end] values. + /// + /// If the [direction] is [Axis.vertical], and either the [mainAxisAlignment] + /// is either [MainAxisAlignment.start] or [MainAxisAlignment.end], or there's + /// more than one child, then the [verticalDirection] must not be null. + /// + /// If the [direction] is [Axis.horizontal], this controls the meaning of the + /// [crossAxisAlignment] property's [CrossAxisAlignment.start] and + /// [CrossAxisAlignment.end] values. + /// + /// If the [direction] is [Axis.horizontal], and the [crossAxisAlignment] is + /// either [CrossAxisAlignment.start] or [CrossAxisAlignment.end], then the + /// [verticalDirection] must not be null. + final VerticalDirection verticalDirection; + /// If aligning items according to their baseline, which baseline to use. final TextBaseline textBaseline; + bool get _needTextDirection { + assert(direction != null); + switch (direction) { + case Axis.horizontal: + return true; // because it affects the layout order. + case Axis.vertical: + assert(crossAxisAlignment != null); + return crossAxisAlignment == CrossAxisAlignment.start + || crossAxisAlignment == CrossAxisAlignment.end; + } + return null; + } + + /// The value to pass to [RenderFlex.textDirection]. + /// + /// This value is derived from the [textDirection] property and the ambient + /// [Directionality]. The value is null if there is no need to specify the + /// text direction. In practice there's always a need to specify the direction + /// except for vertical flexes (e.g. [Column]s) whose [crossAxisAlignment] is + /// not dependent on the text direction (not `start` or `end`). In particular, + /// a [Row] always needs a text direction because the text direction controls + /// its layout order. (For [Column]s, the layout order is controlled by + /// [verticalDirection], which is always specified as it does not depend on an + /// inherited widget and defaults to [VerticalDirection.down].) + /// + /// This method exists so that subclasses of [Flex] that create their own + /// render objects that are derived from [RenderFlex] can do so and still use + /// the logic for providing a text direction only when it is necessary. + @protected + TextDirection getEffectiveTextDirection(BuildContext context) { + return textDirection ?? (_needTextDirection ? Directionality.of(context) : null); + } + @override RenderFlex createRenderObject(BuildContext context) { return new RenderFlex( @@ -2556,7 +2653,9 @@ class Flex extends MultiChildRenderObjectWidget { mainAxisAlignment: mainAxisAlignment, mainAxisSize: mainAxisSize, crossAxisAlignment: crossAxisAlignment, - textBaseline: textBaseline + textDirection: getEffectiveTextDirection(context), + verticalDirection: verticalDirection, + textBaseline: textBaseline, ); } @@ -2567,8 +2666,22 @@ class Flex extends MultiChildRenderObjectWidget { ..mainAxisAlignment = mainAxisAlignment ..mainAxisSize = mainAxisSize ..crossAxisAlignment = crossAxisAlignment + ..textDirection = getEffectiveTextDirection(context) + ..verticalDirection = verticalDirection ..textBaseline = textBaseline; } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder description) { + super.debugFillProperties(description); + description.add(new EnumProperty('direction', direction)); + description.add(new EnumProperty('mainAxisAlignment', mainAxisAlignment)); + description.add(new EnumProperty('mainAxisSize', mainAxisSize, defaultValue: MainAxisSize.max)); + description.add(new EnumProperty('crossAxisAlignment', crossAxisAlignment)); + description.add(new EnumProperty('textDirection', textDirection, defaultValue: null)); + description.add(new EnumProperty('verticalDirection', verticalDirection, defaultValue: VerticalDirection.down)); + description.add(new EnumProperty('textBaseline', textBaseline, defaultValue: null)); + } } /// A widget that displays its children in a horizontal array. @@ -2716,14 +2829,24 @@ class Flex extends MultiChildRenderObjectWidget { class Row extends Flex { /// Creates a horizontal array of children. /// - /// The [direction], [mainAxisAlignment], [mainAxisSize], and - /// [crossAxisAlignment] arguments must not be null. If [crossAxisAlignment] - /// is [CrossAxisAlignment.baseline], then [textBaseline] must not be null. + /// The [direction], [mainAxisAlignment], [mainAxisSize], + /// [crossAxisAlignment], and [verticalDirection] arguments must not be null. + /// If [crossAxisAlignment] is [CrossAxisAlignment.baseline], then + /// [textBaseline] must not be null. + /// + /// The [textDirection] argument defaults to the ambient [Directionality], if + /// any. If there is no ambient directionality, and a text direction is going + /// to be necessary to determine the layout order (which is always the case + /// unless the row has no children or only one child) or to disambiguate + /// `start` or `end` values for the [mainAxisDirection], the [textDirection] + /// must not be null. Row({ Key key, MainAxisAlignment mainAxisAlignment: MainAxisAlignment.start, MainAxisSize mainAxisSize: MainAxisSize.max, CrossAxisAlignment crossAxisAlignment: CrossAxisAlignment.center, + TextDirection textDirection, + VerticalDirection verticalDirection: VerticalDirection.down, TextBaseline textBaseline, List children: const [], }) : super( @@ -2733,7 +2856,9 @@ class Row extends Flex { mainAxisAlignment: mainAxisAlignment, mainAxisSize: mainAxisSize, crossAxisAlignment: crossAxisAlignment, - textBaseline: textBaseline + textDirection: textDirection, + verticalDirection: verticalDirection, + textBaseline: textBaseline, ); } @@ -2843,14 +2968,22 @@ class Row extends Flex { class Column extends Flex { /// Creates a vertical array of children. /// - /// The [direction], [mainAxisAlignment], [mainAxisSize], and - /// [crossAxisAlignment] arguments must not be null. If [crossAxisAlignment] - /// is [CrossAxisAlignment.baseline], then [textBaseline] must not be null. + /// The [direction], [mainAxisAlignment], [mainAxisSize], + /// [crossAxisAlignment], and [verticalDirection] arguments must not be null. + /// If [crossAxisAlignment] is [CrossAxisAlignment.baseline], then + /// [textBaseline] must not be null. + /// + /// The [textDirection] argument defaults to the ambient [Directionality], if + /// any. If there is no ambient directionality, and a text direction is going + /// to be necessary to disambiguate `start` or `end` values for the + /// [crossAxisDirection], the [textDirection] must not be null. Column({ Key key, MainAxisAlignment mainAxisAlignment: MainAxisAlignment.start, MainAxisSize mainAxisSize: MainAxisSize.max, CrossAxisAlignment crossAxisAlignment: CrossAxisAlignment.center, + TextDirection textDirection, + VerticalDirection verticalDirection: VerticalDirection.down, TextBaseline textBaseline, List children: const [], }) : super( @@ -2860,7 +2993,9 @@ class Column extends Flex { mainAxisAlignment: mainAxisAlignment, mainAxisSize: mainAxisSize, crossAxisAlignment: crossAxisAlignment, - textBaseline: textBaseline + textDirection: textDirection, + verticalDirection: verticalDirection, + textBaseline: textBaseline, ); } diff --git a/packages/flutter/lib/src/widgets/container.dart b/packages/flutter/lib/src/widgets/container.dart index 953311ee80..b0ebe09dc4 100644 --- a/packages/flutter/lib/src/widgets/container.dart +++ b/packages/flutter/lib/src/widgets/container.dart @@ -282,7 +282,7 @@ class Container extends StatelessWidget { /// Empty space to inscribe inside the [decoration]. The [child], if any, is /// placed inside this padding. - final EdgeInsets padding; + final EdgeInsetsGeometry padding; /// The decoration to paint behind the [child]. /// @@ -303,18 +303,18 @@ class Container extends StatelessWidget { final BoxConstraints constraints; /// Empty space to surround the [decoration] and [child]. - final EdgeInsets margin; + final EdgeInsetsGeometry margin; /// The transformation matrix to apply before painting the container. final Matrix4 transform; - EdgeInsets get _paddingIncludingDecoration { + EdgeInsetsGeometry get _paddingIncludingDecoration { if (decoration == null || decoration.padding == null) return padding; - final EdgeInsets decorationPadding = decoration.padding; + final EdgeInsetsGeometry decorationPadding = decoration.padding; if (padding == null) return decorationPadding; - return padding + decorationPadding; + return padding.add(decorationPadding); } @override @@ -332,7 +332,7 @@ class Container extends StatelessWidget { if (alignment != null) current = new Align(alignment: alignment, child: current); - final EdgeInsets effectivePadding = _paddingIncludingDecoration; + final EdgeInsetsGeometry effectivePadding = _paddingIncludingDecoration; if (effectivePadding != null) current = new Padding(padding: effectivePadding, child: current); @@ -363,11 +363,11 @@ class Container extends StatelessWidget { void debugFillProperties(DiagnosticPropertiesBuilder description) { super.debugFillProperties(description); description.add(new DiagnosticsProperty('alignment', alignment, showName: false, defaultValue: null)); - description.add(new DiagnosticsProperty('padding', padding, defaultValue: null)); + description.add(new DiagnosticsProperty('padding', padding, defaultValue: null)); description.add(new DiagnosticsProperty('bg', decoration, defaultValue: null)); description.add(new DiagnosticsProperty('fg', foregroundDecoration, defaultValue: null)); description.add(new DiagnosticsProperty('constraints', constraints, defaultValue: null)); - description.add(new DiagnosticsProperty('margin', margin, defaultValue: null)); + description.add(new DiagnosticsProperty('margin', margin, defaultValue: null)); description.add(new ObjectFlagProperty.has('transform', transform)); } } diff --git a/packages/flutter/lib/src/widgets/localizations.dart b/packages/flutter/lib/src/widgets/localizations.dart index 9b63985b79..961fbc6081 100644 --- a/packages/flutter/lib/src/widgets/localizations.dart +++ b/packages/flutter/lib/src/widgets/localizations.dart @@ -7,6 +7,7 @@ import 'dart:ui' show Locale; import 'package:flutter/foundation.dart'; +import 'basic.dart'; import 'binding.dart'; import 'container.dart'; import 'framework.dart'; @@ -90,6 +91,47 @@ abstract class LocalizationsDelegate { /// rebuilt. If it returns true then dependent widgets will be rebuilt /// after [load] has completed. bool shouldReload(covariant LocalizationsDelegate old); + + @override + String toString() => '$runtimeType'; +} + +/// Interface for localized resource values for the lowest levels of the Flutter +/// framework. +/// +/// In particular, this maps locales to a specific [Directionality] using the +/// [textDirection] property. +/// +/// This class provides a default placeholder implementation that returns +/// hard-coded American English values. +class WidgetsLocalizations { + /// Create a placeholder object for the localized resources of the lowest + /// levels of the Flutter framework which only provides values for American + /// English. + const WidgetsLocalizations(); + + /// The locale for which the values of this class's localized resources + /// have been translated. + Locale get locale => const Locale('en', 'US'); + + /// The reading direction for text in this locale. + TextDirection get textDirection => TextDirection.ltr; + + /// The `WidgetsLocalizations` from the closest [Localizations] instance + /// that encloses the given context. + /// + /// This method is just a convenient shorthand for: + /// `Localizations.of(context, WidgetsLocalizations)`. + /// + /// References to the localized resources defined by this class are typically + /// written in terms of this method. For example: + /// + /// ```dart + /// textDirection: WidgetsLocalizations.of(context).textDirection, + /// ``` + static WidgetsLocalizations of(BuildContext context) { + return Localizations.of(context, WidgetsLocalizations); + } } class _LocalizationsScope extends InheritedWidget { @@ -158,7 +200,7 @@ class _LocalizationsScope extends InheritedWidget { /// /// This class is effectively an [InheritedWidget]. If it's rebuilt with /// a new `locale` or a different list of delegates or any of its -/// delegates' [LocalizationDelegate.shouldReload()] methods returns true, +/// delegates' [LocalizationsDelegate.shouldReload()] methods returns true, /// then widgets that have created a dependency by calling /// `Localizations.of(context)` will be rebuilt after the resources /// for the new locale have been loaded. @@ -199,21 +241,25 @@ class _LocalizationsScope extends InheritedWidget { /// One could choose another approach for loading localized resources and looking them up while /// still conforming to the structure of this example. class Localizations extends StatefulWidget { + /// Create a widget from which ambient localizations (translated strings) + /// can be obtained. Localizations({ Key key, @required this.locale, - this.delegates, - this.child - }) : super(key: key) { - assert(locale != null); + @required this.delegates, + this.child, + }) : assert(locale != null), + assert(delegates != null), + super(key: key) { + assert(delegates.any((LocalizationsDelegate delegate) => delegate is LocalizationsDelegate)); } /// The resources returned by [Localizations.of] will be specific to this locale. final Locale locale; /// This list collectively defines the localized resources objects that can - /// be retrieved with [ Localizations.of]. - final Iterable> delegates; + /// be retrieved with [Localizations.of]. + final List> delegates; /// The widget below this widget in the tree. final Widget child; @@ -247,6 +293,13 @@ class Localizations extends StatefulWidget { @override _LocalizationsState createState() => new _LocalizationsState(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder description) { + super.debugFillProperties(description); + description.add(new DiagnosticsProperty('locale', locale)); + description.add(new IterableProperty>('delegates', delegates)); + } } class _LocalizationsState extends State { @@ -329,18 +382,29 @@ class _LocalizationsState extends State { T resourcesFor(Type type) { assert(type != null); - final dynamic resources = _typeToResources[type]; + final T resources = _typeToResources[type]; assert(resources.runtimeType == type); return resources; } + TextDirection get _textDirection { + final WidgetsLocalizations resources = _typeToResources[WidgetsLocalizations]; + assert(resources != null); + return resources.textDirection; + } + @override Widget build(BuildContext context) { + if (_locale == null) + return new Container(); return new _LocalizationsScope( key: _localizedResourcesScopeKey, - locale: widget.locale, + locale: _locale, localizationsState: this, - child: _locale != null ? widget.child : new Container(), + child: new Directionality( + textDirection: _textDirection, + child: widget.child, + ), ); } } diff --git a/packages/flutter/lib/src/widgets/scroll_controller.dart b/packages/flutter/lib/src/widgets/scroll_controller.dart index b4b078910b..50476b8f3e 100644 --- a/packages/flutter/lib/src/widgets/scroll_controller.dart +++ b/packages/flutter/lib/src/widgets/scroll_controller.dart @@ -264,7 +264,7 @@ class ScrollController extends ChangeNotifier { description.add('no clients'); } else if (_positions.length == 1) { // Don't actually list the client itself, since its toString may refer to us. - description.add('one client, offset ${offset.toStringAsFixed(1)}'); + description.add('one client, offset ${offset?.toStringAsFixed(1)}'); } else { description.add('${_positions.length} clients'); } diff --git a/packages/flutter/test/cupertino/bottom_tab_bar_test.dart b/packages/flutter/test/cupertino/bottom_tab_bar_test.dart index 9005414c86..db765c249a 100644 --- a/packages/flutter/test/cupertino/bottom_tab_bar_test.dart +++ b/packages/flutter/test/cupertino/bottom_tab_bar_test.dart @@ -7,10 +7,19 @@ import 'package:flutter_test/flutter_test.dart'; import '../services/mocks_for_image_cache.dart'; +Future pumpWidgetWithBoilerplate(WidgetTester tester, Widget widget) async { + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.ltr, + child: widget, + ), + ); +} + void main() { testWidgets('Need at least 2 tabs', (WidgetTester tester) async { try { - await tester.pumpWidget(new CupertinoTabBar( + await pumpWidgetWithBoilerplate(tester, new CupertinoTabBar( items: [ const BottomNavigationBarItem( icon: const ImageIcon(const TestImageProvider(24, 24)), @@ -19,13 +28,14 @@ void main() { ], )); fail('Should not be possible to create a tab bar with just one item'); - } on AssertionError { + } on AssertionError catch (e) { + expect(e.toString(), contains('items.length')); // Exception expected. } }); testWidgets('Active and inactive colors', (WidgetTester tester) async { - await tester.pumpWidget(new CupertinoTabBar( + await pumpWidgetWithBoilerplate(tester, new CupertinoTabBar( items: [ const BottomNavigationBarItem( icon: const ImageIcon(const TestImageProvider(24, 24)), @@ -55,7 +65,7 @@ void main() { }); testWidgets('Opaque background does not add blur effects', (WidgetTester tester) async { - await tester.pumpWidget(new CupertinoTabBar( + await pumpWidgetWithBoilerplate(tester, new CupertinoTabBar( items: [ const BottomNavigationBarItem( icon: const ImageIcon(const TestImageProvider(24, 24)), @@ -70,7 +80,7 @@ void main() { expect(find.byType(BackdropFilter), findsOneWidget); - await tester.pumpWidget(new CupertinoTabBar( + await pumpWidgetWithBoilerplate(tester, new CupertinoTabBar( items: [ const BottomNavigationBarItem( icon: const ImageIcon(const TestImageProvider(24, 24)), @@ -90,7 +100,7 @@ void main() { testWidgets('Tap callback', (WidgetTester tester) async { int callbackTab; - await tester.pumpWidget(new CupertinoTabBar( + await pumpWidgetWithBoilerplate(tester, new CupertinoTabBar( items: [ const BottomNavigationBarItem( icon: const ImageIcon(const TestImageProvider(24, 24)), diff --git a/packages/flutter/test/material/about_test.dart b/packages/flutter/test/material/about_test.dart index f2b9fe0529..223e1bb581 100644 --- a/packages/flutter/test/material/about_test.dart +++ b/packages/flutter/test/material/about_test.dart @@ -67,7 +67,10 @@ void main() { testWidgets('About box logic defaults to executable name for app name', (WidgetTester tester) async { await tester.pumpWidget( - const Material(child: const AboutListTile()), + const Directionality( + textDirection: TextDirection.ltr, + child: const Material(child: const AboutListTile()), + ), ); expect(find.text('About flutter_tester'), findsOneWidget); }); diff --git a/packages/flutter/test/material/app_bar_test.dart b/packages/flutter/test/material/app_bar_test.dart index 298fa0d818..8ad2c3ebdf 100644 --- a/packages/flutter/test/material/app_bar_test.dart +++ b/packages/flutter/test/material/app_bar_test.dart @@ -8,31 +8,34 @@ import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; Widget buildSliverAppBarApp({ bool floating, bool pinned, double expandedHeight, bool snap: false }) { - return new MediaQuery( - data: const MediaQueryData(), - child: new Scaffold( - body: new DefaultTabController( - length: 3, - child: new CustomScrollView( - primary: true, - slivers: [ - new SliverAppBar( - title: const Text('AppBar Title'), - floating: floating, - pinned: pinned, - expandedHeight: expandedHeight, - snap: snap, - bottom: new TabBar( - tabs: ['A','B','C'].map((String t) => new Tab(text: 'TAB $t')).toList(), + return new Directionality( + textDirection: TextDirection.ltr, + child: new MediaQuery( + data: const MediaQueryData(), + child: new Scaffold( + body: new DefaultTabController( + length: 3, + child: new CustomScrollView( + primary: true, + slivers: [ + new SliverAppBar( + title: const Text('AppBar Title'), + floating: floating, + pinned: pinned, + expandedHeight: expandedHeight, + snap: snap, + bottom: new TabBar( + tabs: ['A','B','C'].map((String t) => new Tab(text: 'TAB $t')).toList(), + ), ), - ), - new SliverToBoxAdapter( - child: new Container( - height: 1200.0, - color: Colors.orange[400], + new SliverToBoxAdapter( + child: new Container( + height: 1200.0, + color: Colors.orange[400], + ), ), - ), - ], + ], + ), ), ), ), @@ -699,11 +702,14 @@ void main() { const MediaQueryData topPadding100 = const MediaQueryData(padding: const EdgeInsets.only(top: 100.0)); await tester.pumpWidget( - new MediaQuery( - data: topPadding100, - child: new Scaffold( - primary: false, - appBar: new AppBar(), + new Directionality( + textDirection: TextDirection.ltr, + child: new MediaQuery( + data: topPadding100, + child: new Scaffold( + primary: false, + appBar: new AppBar(), + ), ), ), ); @@ -711,11 +717,14 @@ void main() { expect(appBarHeight(tester), kToolbarHeight); await tester.pumpWidget( - new MediaQuery( - data: topPadding100, - child: new Scaffold( - primary: true, - appBar: new AppBar(title: const Text('title')) + new Directionality( + textDirection: TextDirection.ltr, + child: new MediaQuery( + data: topPadding100, + child: new Scaffold( + primary: true, + appBar: new AppBar(title: const Text('title')) + ), ), ), ); @@ -724,14 +733,17 @@ void main() { expect(appBarHeight(tester), kToolbarHeight + 100.0); await tester.pumpWidget( - new MediaQuery( - data: topPadding100, - child: new Scaffold( - primary: false, - appBar: new AppBar( - bottom: new PreferredSize( - preferredSize: const Size.fromHeight(200.0), - child: new Container(), + new Directionality( + textDirection: TextDirection.ltr, + child: new MediaQuery( + data: topPadding100, + child: new Scaffold( + primary: false, + appBar: new AppBar( + bottom: new PreferredSize( + preferredSize: const Size.fromHeight(200.0), + child: new Container(), + ), ), ), ), @@ -741,14 +753,17 @@ void main() { expect(appBarHeight(tester), kToolbarHeight + 200.0); await tester.pumpWidget( - new MediaQuery( - data: topPadding100, - child: new Scaffold( - primary: true, - appBar: new AppBar( - bottom: new PreferredSize( - preferredSize: const Size.fromHeight(200.0), - child: new Container(), + new Directionality( + textDirection: TextDirection.ltr, + child: new MediaQuery( + data: topPadding100, + child: new Scaffold( + primary: true, + appBar: new AppBar( + bottom: new PreferredSize( + preferredSize: const Size.fromHeight(200.0), + child: new Container(), + ), ), ), ), @@ -758,11 +773,14 @@ void main() { expect(appBarHeight(tester), kToolbarHeight + 100.0 + 200.0); await tester.pumpWidget( - new MediaQuery( - data: topPadding100, - child: new AppBar( - primary: false, - title: const Text('title'), + new Directionality( + textDirection: TextDirection.ltr, + child: new MediaQuery( + data: topPadding100, + child: new AppBar( + primary: false, + title: const Text('title'), + ), ), ), ); diff --git a/packages/flutter/test/material/button_bar_test.dart b/packages/flutter/test/material/button_bar_test.dart index 37d9339279..f4f4226ad3 100644 --- a/packages/flutter/test/material/button_bar_test.dart +++ b/packages/flutter/test/material/button_bar_test.dart @@ -6,7 +6,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { - testWidgets('ButtonBar default control', (WidgetTester tester) async { - await tester.pumpWidget(const Center(child: const ButtonBar())); + testWidgets('ButtonBar default control smoketest', (WidgetTester tester) async { + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.ltr, + child: const ButtonBar(), + ), + ); }); } diff --git a/packages/flutter/test/material/chip_test.dart b/packages/flutter/test/material/chip_test.dart index 606995d1ef..092e2f7b9e 100644 --- a/packages/flutter/test/material/chip_test.dart +++ b/packages/flutter/test/material/chip_test.dart @@ -107,29 +107,31 @@ void main() { feedback.dispose(); }); - testWidgets( - 'Chip does not constrain size of label widget if it does not exceed ' - 'the available space', (WidgetTester tester) async { + testWidgets('Chip does not constrain size of label widget if it does not exceed ' + 'the available space', (WidgetTester tester) async { const double labelWidth = 50.0; const double labelHeight = 30.0; final Key labelKey = new UniqueKey(); await tester.pumpWidget( - new Material( - child: new Center( - child: new Container( - width: 500.0, - height: 500.0, - child: new Column( - children: [ - new Chip( - label: new Container( - key: labelKey, - width: labelWidth, - height: labelHeight, + new Directionality( + textDirection: TextDirection.ltr, + child: new Material( + child: new Center( + child: new Container( + width: 500.0, + height: 500.0, + child: new Column( + children: [ + new Chip( + label: new Container( + key: labelKey, + width: labelWidth, + height: labelHeight, + ), ), - ), - ], + ], + ), ), ), ), @@ -141,15 +143,13 @@ void main() { expect(labelSize.height, labelHeight); }); - testWidgets( - 'Chip constrains the size of the label widget when it exceeds the ' - 'available space', (WidgetTester tester) async { + testWidgets('Chip constrains the size of the label widget when it exceeds the ' + 'available space', (WidgetTester tester) async { await _testConstrainedLabel(tester); }); - testWidgets( - 'Chip constrains the size of the label widget when it exceeds the ' - 'available space and the avatar is present', (WidgetTester tester) async { + testWidgets('Chip constrains the size of the label widget when it exceeds the ' + 'available space and the avatar is present', (WidgetTester tester) async { await _testConstrainedLabel( tester, avatar: const CircleAvatar( @@ -158,20 +158,16 @@ void main() { ); }); - testWidgets( - 'Chip constrains the size of the label widget when it exceeds the ' - 'available space and the delete icon is present', - (WidgetTester tester) async { + testWidgets('Chip constrains the size of the label widget when it exceeds the ' + 'available space and the delete icon is present', (WidgetTester tester) async { await _testConstrainedLabel( tester, onDeleted: () {}, ); }); - testWidgets( - 'Chip constrains the size of the label widget when it exceeds the ' - 'available space and both avatar and delete icons are present', - (WidgetTester tester) async { + testWidgets('Chip constrains the size of the label widget when it exceeds the ' + 'available space and both avatar and delete icons are present', (WidgetTester tester) async { await _testConstrainedLabel( tester, avatar: const CircleAvatar( @@ -223,4 +219,104 @@ void main() { expect(tester.getSize(find.byType(Text)), const Size(40.0, 10.0)); expect(tester.getSize(find.byType(Chip)), const Size(800.0, 32.0)); }); + testWidgets('Chip supports RTL', (WidgetTester tester) async { + final Widget test = new Overlay( + initialEntries: [ + new OverlayEntry( + builder: (BuildContext context) { + return new Material( + child: new Center( + child: new Chip( + onDeleted: () { }, + label: new Text('ABC'), + ), + ), + ); + }, + ), + ], + ); + + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.rtl, + child: test, + ), + ); + expect(tester.getCenter(find.text('ABC')).dx, greaterThan(tester.getCenter(find.byType(Icon)).dx)); + + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.ltr, + child: test, + ), + ); + expect(tester.getCenter(find.text('ABC')).dx, lessThan(tester.getCenter(find.byType(Icon)).dx)); + }); + + testWidgets('Chip padding - LTR', (WidgetTester tester) async { + final GlobalKey keyA = new GlobalKey(); + final GlobalKey keyB = new GlobalKey(); + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.ltr, + child: new Overlay( + initialEntries: [ + new OverlayEntry( + builder: (BuildContext context) { + return new Material( + child: new Center( + child: new Chip( + avatar: new Placeholder(key: keyA), + label: new Placeholder(key: keyB), + onDeleted: () { }, + ), + ), + ); + }, + ), + ], + ), + ), + ); + expect(tester.getTopLeft(find.byKey(keyA)), const Offset(0.0, 284.0)); + expect(tester.getBottomRight(find.byKey(keyA)), const Offset(32.0, 316.0)); + expect(tester.getTopLeft(find.byKey(keyB)), const Offset(40.0, 284.0)); + expect(tester.getBottomRight(find.byKey(keyB)), const Offset(774.0, 316.0)); + expect(tester.getTopLeft(find.byType(Icon)), const Offset(778.0, 291.0)); + expect(tester.getBottomRight(find.byType(Icon)), const Offset(796.0, 309.0)); + }); + + testWidgets('Chip padding - RTL', (WidgetTester tester) async { + final GlobalKey keyA = new GlobalKey(); + final GlobalKey keyB = new GlobalKey(); + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.rtl, + child: new Overlay( + initialEntries: [ + new OverlayEntry( + builder: (BuildContext context) { + return new Material( + child: new Center( + child: new Chip( + avatar: new Placeholder(key: keyA), + label: new Placeholder(key: keyB), + onDeleted: () { }, + ), + ), + ); + }, + ), + ], + ), + ), + ); + expect(tester.getTopRight(find.byKey(keyA)), const Offset(800.0 - 0.0, 284.0)); + expect(tester.getBottomLeft(find.byKey(keyA)), const Offset(800.0 - 32.0, 316.0)); + expect(tester.getTopRight(find.byKey(keyB)), const Offset(800.0 - 40.0, 284.0)); + expect(tester.getBottomLeft(find.byKey(keyB)), const Offset(800.0 - 774.0, 316.0)); + expect(tester.getTopRight(find.byType(Icon)), const Offset(800.0 - 778.0, 291.0)); + expect(tester.getBottomLeft(find.byType(Icon)), const Offset(800.0 - 796.0, 309.0)); + }); } diff --git a/packages/flutter/test/material/control_list_tile_test.dart b/packages/flutter/test/material/control_list_tile_test.dart index b7697e49a4..3550ea0bf6 100644 --- a/packages/flutter/test/material/control_list_tile_test.dart +++ b/packages/flutter/test/material/control_list_tile_test.dart @@ -10,10 +10,17 @@ import 'package:flutter/rendering.dart'; import '../widgets/semantics_tester.dart'; +Widget wrap({ Widget child }) { + return new Directionality( + textDirection: TextDirection.ltr, + child: new Material(child: child), + ); +} + void main() { testWidgets('CheckboxListTile control test', (WidgetTester tester) async { final List log = []; - await tester.pumpWidget(new Material( + await tester.pumpWidget(wrap( child: new CheckboxListTile( value: true, onChanged: (bool value) { log.add(value); }, @@ -28,7 +35,7 @@ void main() { testWidgets('RadioListTile control test', (WidgetTester tester) async { final List log = []; - await tester.pumpWidget(new Material( + await tester.pumpWidget(wrap( child: new RadioListTile( value: true, groupValue: false, @@ -44,7 +51,7 @@ void main() { testWidgets('SwitchListTile control test', (WidgetTester tester) async { final List log = []; - await tester.pumpWidget(new Material( + await tester.pumpWidget(wrap( child: new SwitchListTile( value: true, onChanged: (bool value) { log.add(value); }, @@ -59,7 +66,7 @@ void main() { testWidgets('SwitchListTile control test', (WidgetTester tester) async { final SemanticsTester semantics = new SemanticsTester(tester); - await tester.pumpWidget(new Material( + await tester.pumpWidget(wrap( child: new Column( children: [ new SwitchListTile( diff --git a/packages/flutter/test/material/expansion_panel_test.dart b/packages/flutter/test/material/expansion_panel_test.dart index e474e0f7f1..64e16abd89 100644 --- a/packages/flutter/test/material/expansion_panel_test.dart +++ b/packages/flutter/test/material/expansion_panel_test.dart @@ -11,22 +11,24 @@ void main() { bool isExpanded; await tester.pumpWidget( - new SingleChildScrollView( - child: new ExpansionPanelList( - expansionCallback: (int _index, bool _isExpanded) { - index = _index; - isExpanded = _isExpanded; - }, - children: [ - new ExpansionPanel( - headerBuilder: (BuildContext context, bool isExpanded) { - return new Text(isExpanded ? 'B' : 'A'); - }, - body: const SizedBox(height: 100.0) - ) - ] - ) - ) + new MaterialApp( + home: new SingleChildScrollView( + child: new ExpansionPanelList( + expansionCallback: (int _index, bool _isExpanded) { + index = _index; + isExpanded = _isExpanded; + }, + children: [ + new ExpansionPanel( + headerBuilder: (BuildContext context, bool isExpanded) { + return new Text(isExpanded ? 'B' : 'A'); + }, + body: const SizedBox(height: 100.0), + ), + ], + ), + ), + ), ); expect(find.text('A'), findsOneWidget); @@ -42,28 +44,28 @@ void main() { // now expand the child panel await tester.pumpWidget( - new SingleChildScrollView( - child: new ExpansionPanelList( - expansionCallback: (int _index, bool _isExpanded) { - index = _index; - isExpanded = _isExpanded; - }, - children: [ - new ExpansionPanel( - headerBuilder: (BuildContext context, bool isExpanded) { - return new Text(isExpanded ? 'B' : 'A'); - }, - body: const SizedBox(height: 100.0), - isExpanded: true // this is the addition - ) - ] - ) - ) + new MaterialApp( + home: new SingleChildScrollView( + child: new ExpansionPanelList( + expansionCallback: (int _index, bool _isExpanded) { + index = _index; + isExpanded = _isExpanded; + }, + children: [ + new ExpansionPanel( + headerBuilder: (BuildContext context, bool isExpanded) { + return new Text(isExpanded ? 'B' : 'A'); + }, + body: const SizedBox(height: 100.0), + isExpanded: true, // this is the addition + ), + ], + ), + ), + ), ); - await tester.pump(const Duration(milliseconds: 200)); - expect(find.text('A'), findsNothing); expect(find.text('B'), findsOneWidget); box = tester.renderObject(find.byType(ExpansionPanelList)); diff --git a/packages/flutter/test/material/grid_title_test.dart b/packages/flutter/test/material/grid_title_test.dart index b072303583..5cf53a6997 100644 --- a/packages/flutter/test/material/grid_title_test.dart +++ b/packages/flutter/test/material/grid_title_test.dart @@ -10,23 +10,25 @@ void main() { final Key headerKey = new UniqueKey(); final Key footerKey = new UniqueKey(); - await tester.pumpWidget(new GridTile( - header: new GridTileBar( - key: headerKey, - leading: const Icon(Icons.thumb_up), - title: const Text('Header'), - subtitle: const Text('Subtitle'), - trailing: const Icon(Icons.thumb_up), - ), - child: new DecoratedBox( - decoration: new BoxDecoration( - color: Colors.green[500], + await tester.pumpWidget(new MaterialApp( + home: new GridTile( + header: new GridTileBar( + key: headerKey, + leading: const Icon(Icons.thumb_up), + title: const Text('Header'), + subtitle: const Text('Subtitle'), + trailing: const Icon(Icons.thumb_up), + ), + child: new DecoratedBox( + decoration: new BoxDecoration( + color: Colors.green[500], + ), + ), + footer: new GridTileBar( + key: footerKey, + title: const Text('Footer'), + backgroundColor: Colors.black38, ), - ), - footer: new GridTileBar( - key: footerKey, - title: const Text('Footer'), - backgroundColor: Colors.black38, ), )); diff --git a/packages/flutter/test/material/icon_button_test.dart b/packages/flutter/test/material/icon_button_test.dart index 1d9ed34a5b..6d7a2f57c5 100644 --- a/packages/flutter/test/material/icon_button_test.dart +++ b/packages/flutter/test/material/icon_button_test.dart @@ -98,15 +98,18 @@ void main() { 'test default icon buttons can be stretched if specified', (WidgetTester tester) async { await tester.pumpWidget( - new Material( - child: new Row( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - new IconButton( - onPressed: mockOnPressedFunction, - icon: const Icon(Icons.ac_unit), - ), - ], + new Directionality( + textDirection: TextDirection.ltr, + child: new Material( + child: new Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + new IconButton( + onPressed: mockOnPressedFunction, + icon: const Icon(Icons.ac_unit), + ), + ], + ), ), ), ); diff --git a/packages/flutter/test/material/input_decorator_test.dart b/packages/flutter/test/material/input_decorator_test.dart index 81e41a11fc..49c323b518 100644 --- a/packages/flutter/test/material/input_decorator_test.dart +++ b/packages/flutter/test/material/input_decorator_test.dart @@ -9,37 +9,43 @@ void main() { testWidgets('InputDecorator always expands horizontally', (WidgetTester tester) async { final Key key = new UniqueKey(); - await tester.pumpWidget(new Material( - child: new Center( - child: new InputDecorator( - decoration: const InputDecoration(), - child: new Container(key: key, width: 50.0, height: 60.0, color: Colors.blue), + await tester.pumpWidget(new MaterialApp( + home: new Material( + child: new Center( + child: new InputDecorator( + decoration: const InputDecoration(), + child: new Container(key: key, width: 50.0, height: 60.0, color: Colors.blue), + ), ), ), )); expect(tester.element(find.byKey(key)).size, equals(const Size(800.0, 60.0))); - await tester.pumpWidget(new Material( - child: new Center( - child: new InputDecorator( - decoration: const InputDecoration( - icon: const Icon(Icons.add_shopping_cart), + await tester.pumpWidget(new MaterialApp( + home: new Material( + child: new Center( + child: new InputDecorator( + decoration: const InputDecoration( + icon: const Icon(Icons.add_shopping_cart), + ), + child: new Container(key: key, width: 50.0, height: 60.0, color: Colors.blue), ), - child: new Container(key: key, width: 50.0, height: 60.0, color: Colors.blue), ), ), )); expect(tester.element(find.byKey(key)).size, equals(const Size(752.0, 60.0))); - await tester.pumpWidget(new Material( - child: new Center( - child: new InputDecorator( - decoration: const InputDecoration.collapsed( - hintText: 'Hint text', + await tester.pumpWidget(new MaterialApp( + home: new Material( + child: new Center( + child: new InputDecorator( + decoration: const InputDecoration.collapsed( + hintText: 'Hint text', + ), + child: new Container(key: key, width: 50.0, height: 60.0, color: Colors.blue), ), - child: new Container(key: key, width: 50.0, height: 60.0, color: Colors.blue), ), ), )); diff --git a/packages/flutter/test/material/page_selector_test.dart b/packages/flutter/test/material/page_selector_test.dart index ca1a65e7c4..8abe971740 100644 --- a/packages/flutter/test/material/page_selector_test.dart +++ b/packages/flutter/test/material/page_selector_test.dart @@ -9,32 +9,35 @@ const Color kSelectedColor = const Color(0xFF00FF00); const Color kUnselectedColor = Colors.transparent; Widget buildFrame(TabController tabController, { Color color, Color selectedColor, double indicatorSize: 12.0 }) { - return new Theme( - data: new ThemeData(accentColor: kSelectedColor), - child: new SizedBox.expand( - child: new Center( - child: new SizedBox( - width: 400.0, - height: 400.0, - child: new Column( - children: [ - new TabPageSelector( - controller: tabController, - color: color, - selectedColor: selectedColor, - indicatorSize: indicatorSize, - ), - new Flexible( - child: new TabBarView( + return new Directionality( + textDirection: TextDirection.ltr, + child: new Theme( + data: new ThemeData(accentColor: kSelectedColor), + child: new SizedBox.expand( + child: new Center( + child: new SizedBox( + width: 400.0, + height: 400.0, + child: new Column( + children: [ + new TabPageSelector( controller: tabController, - children: [ - const Center(child: const Text('0')), - const Center(child: const Text('1')), - const Center(child: const Text('2')), - ], + color: color, + selectedColor: selectedColor, + indicatorSize: indicatorSize, ), - ), - ], + new Flexible( + child: new TabBarView( + controller: tabController, + children: [ + const Center(child: const Text('0')), + const Center(child: const Text('1')), + const Center(child: const Text('2')), + ], + ), + ), + ], + ), ), ), ), diff --git a/packages/flutter/test/material/stepper_test.dart b/packages/flutter/test/material/stepper_test.dart index fa552eb67b..88cb72a04a 100644 --- a/packages/flutter/test/material/stepper_test.dart +++ b/packages/flutter/test/material/stepper_test.dart @@ -10,29 +10,31 @@ void main() { int index = 0; await tester.pumpWidget( - new Material( - child: new Stepper( - onStepTapped: (int i) { - index = i; - }, - steps: [ - const Step( - title: const Text('Step 1'), - content: const SizedBox( - width: 100.0, - height: 100.0 - ) - ), - const Step( - title: const Text('Step 2'), - content: const SizedBox( - width: 100.0, - height: 100.0 - ) - ) - ] - ) - ) + new MaterialApp( + home: new Material( + child: new Stepper( + onStepTapped: (int i) { + index = i; + }, + steps: [ + const Step( + title: const Text('Step 1'), + content: const SizedBox( + width: 100.0, + height: 100.0, + ), + ), + const Step( + title: const Text('Step 2'), + content: const SizedBox( + width: 100.0, + height: 100.0, + ), + ), + ], + ), + ), + ), ); await tester.tap(find.text('Step 2')); expect(index, 1); @@ -40,57 +42,61 @@ void main() { testWidgets('Stepper expansion test', (WidgetTester tester) async { await tester.pumpWidget( - new Center( - child: new Material( - child: new Stepper( - steps: [ - const Step( - title: const Text('Step 1'), - content: const SizedBox( - width: 100.0, - height: 100.0 - ) - ), - const Step( - title: const Text('Step 2'), - content: const SizedBox( - width: 200.0, - height: 200.0 - ) - ) - ] - ) - ) - ) + new MaterialApp( + home: new Center( + child: new Material( + child: new Stepper( + steps: [ + const Step( + title: const Text('Step 1'), + content: const SizedBox( + width: 100.0, + height: 100.0, + ), + ), + const Step( + title: const Text('Step 2'), + content: const SizedBox( + width: 200.0, + height: 200.0, + ), + ), + ], + ), + ), + ), + ), ); RenderBox box = tester.renderObject(find.byType(Stepper)); expect(box.size.height, 332.0); await tester.pumpWidget( - new Center( - child: new Material( - child: new Stepper( - currentStep: 1, - steps: [ - const Step( - title: const Text('Step 1'), - content: const SizedBox( - width: 100.0, - height: 100.0 - ) - ), - const Step( - title: const Text('Step 2'), - content: const SizedBox( - width: 200.0, - height: 200.0 - ) - ) - ] - ) - ) - ) + new MaterialApp( + home: new Center( + child: new Material( + child: new Stepper( + currentStep: 1, + steps: [ + const Step( + title: const Text('Step 1'), + content: const SizedBox( + width: 100.0, + height: 100.0, + ), + ), + const Step( + title: const Text('Step 2'), + content: const SizedBox( + width: 200.0, + height: 200.0, + ), + ), + ], + ), + ), + ), + ), ); await tester.pump(const Duration(milliseconds: 100)); @@ -103,22 +109,24 @@ void main() { testWidgets('Stepper horizontal size test', (WidgetTester tester) async { await tester.pumpWidget( - new Center( - child: new Material( - child: new Stepper( - type: StepperType.horizontal, - steps: [ - const Step( - title: const Text('Step 1'), - content: const SizedBox( - width: 100.0, - height: 100.0 - ) - ) - ] - ) - ) - ) + new MaterialApp( + home: new Center( + child: new Material( + child: new Stepper( + type: StepperType.horizontal, + steps: [ + const Step( + title: const Text('Step 1'), + content: const SizedBox( + width: 100.0, + height: 100.0, + ), + ), + ], + ), + ), + ), + ), ); final RenderBox box = tester.renderObject(find.byType(Stepper)); @@ -127,43 +135,47 @@ void main() { testWidgets('Stepper visibility test', (WidgetTester tester) async { await tester.pumpWidget( - new Material( - child: new Stepper( - type: StepperType.horizontal, - steps: [ - const Step( - title: const Text('Step 1'), - content: const Text('A') - ), - const Step( - title: const Text('Step 2'), - content: const Text('B') - ) - ] - ) - ) + new MaterialApp( + home: new Material( + child: new Stepper( + type: StepperType.horizontal, + steps: [ + const Step( + title: const Text('Step 1'), + content: const Text('A'), + ), + const Step( + title: const Text('Step 2'), + content: const Text('B'), + ), + ], + ), + ), + ), ); expect(find.text('A'), findsOneWidget); expect(find.text('B'), findsNothing); await tester.pumpWidget( - new Material( - child: new Stepper( - currentStep: 1, - type: StepperType.horizontal, - steps: [ - const Step( - title: const Text('Step 1'), - content: const Text('A') - ), - const Step( - title: const Text('Step 2'), - content: const Text('B') - ) - ] - ) - ) + new MaterialApp( + home: new Material( + child: new Stepper( + currentStep: 1, + type: StepperType.horizontal, + steps: [ + const Step( + title: const Text('Step 1'), + content: const Text('A'), + ), + const Step( + title: const Text('Step 2'), + content: const Text('B'), + ), + ], + ), + ), + ), ); expect(find.text('A'), findsNothing); @@ -175,33 +187,35 @@ void main() { bool cancelPressed = false; await tester.pumpWidget( - new Material( - child: new Stepper( - type: StepperType.horizontal, - onStepContinue: () { - continuePressed = true; - }, - onStepCancel: () { - cancelPressed = true; - }, - steps: [ - const Step( - title: const Text('Step 1'), - content: const SizedBox( - width: 100.0, - height: 100.0 - ) - ), - const Step( - title: const Text('Step 2'), - content: const SizedBox( - width: 200.0, - height: 200.0 - ) - ) - ] - ) - ) + new MaterialApp( + home: new Material( + child: new Stepper( + type: StepperType.horizontal, + onStepContinue: () { + continuePressed = true; + }, + onStepCancel: () { + cancelPressed = true; + }, + steps: [ + const Step( + title: const Text('Step 1'), + content: const SizedBox( + width: 100.0, + height: 100.0, + ), + ), + const Step( + title: const Text('Step 2'), + content: const SizedBox( + width: 200.0, + height: 200.0, + ), + ), + ], + ), + ), + ), ); await tester.tap(find.text('CONTINUE')); @@ -215,30 +229,32 @@ void main() { int index = 0; await tester.pumpWidget( - new Material( - child: new Stepper( - onStepTapped: (int i) { - index = i; - }, - steps: [ - const Step( - title: const Text('Step 1'), - content: const SizedBox( - width: 100.0, - height: 100.0 - ) - ), - const Step( - title: const Text('Step 2'), - state: StepState.disabled, - content: const SizedBox( - width: 100.0, - height: 100.0 - ) - ) - ] - ) - ) + new MaterialApp( + home: new Material( + child: new Stepper( + onStepTapped: (int i) { + index = i; + }, + steps: [ + const Step( + title: const Text('Step 1'), + content: const SizedBox( + width: 100.0, + height: 100.0, + ), + ), + const Step( + title: const Text('Step 2'), + state: StepState.disabled, + content: const SizedBox( + width: 100.0, + height: 100.0, + ), + ), + ], + ), + ), + ), ); await tester.tap(find.text('Step 2')); @@ -247,33 +263,35 @@ void main() { testWidgets('Stepper scroll test', (WidgetTester tester) async { await tester.pumpWidget( - new Material( - child: new Stepper( - steps: [ - const Step( - title: const Text('Step 1'), - content: const SizedBox( - width: 100.0, - height: 300.0 - ) - ), - const Step( - title: const Text('Step 2'), - content: const SizedBox( - width: 100.0, - height: 300.0 - ) - ), - const Step( - title: const Text('Step 3'), - content: const SizedBox( - width: 100.0, - height: 100.0 - ) - ), - ] - ) - ) + new MaterialApp( + home: new Material( + child: new Stepper( + steps: [ + const Step( + title: const Text('Step 1'), + content: const SizedBox( + width: 100.0, + height: 300.0, + ), + ), + const Step( + title: const Text('Step 2'), + content: const SizedBox( + width: 100.0, + height: 300.0, + ), + ), + const Step( + title: const Text('Step 3'), + content: const SizedBox( + width: 100.0, + height: 100.0, + ), + ), + ], + ), + ), + ), ); final ScrollableState scrollableState = tester.firstState(find.byType(Scrollable)); @@ -281,34 +299,36 @@ void main() { await tester.tap(find.text('Step 3')); await tester.pumpWidget( - new Material( - child: new Stepper( - currentStep: 2, - steps: [ - const Step( - title: const Text('Step 1'), - content: const SizedBox( - width: 100.0, - height: 300.0 - ) - ), - const Step( - title: const Text('Step 2'), - content: const SizedBox( - width: 100.0, - height: 300.0 - ) - ), - const Step( - title: const Text('Step 3'), - content: const SizedBox( - width: 100.0, - height: 100.0 - ) - ), - ] - ) - ) + new MaterialApp( + home: new Material( + child: new Stepper( + currentStep: 2, + steps: [ + const Step( + title: const Text('Step 1'), + content: const SizedBox( + width: 100.0, + height: 300.0, + ), + ), + const Step( + title: const Text('Step 2'), + content: const SizedBox( + width: 100.0, + height: 300.0, + ), + ), + const Step( + title: const Text('Step 3'), + content: const SizedBox( + width: 100.0, + height: 100.0, + ), + ), + ], + ), + ), + ), ); await tester.pump(const Duration(milliseconds: 100)); @@ -317,29 +337,31 @@ void main() { testWidgets('Stepper index test', (WidgetTester tester) async { await tester.pumpWidget( - new Center( - child: new Material( - child: new Stepper( - steps: [ - const Step( - title: const Text('A'), - state: StepState.complete, - content: const SizedBox( - width: 100.0, - height: 100.0 - ) - ), - const Step( - title: const Text('B'), - content: const SizedBox( - width: 100.0, - height: 100.0 - ) - ) - ] - ) - ) - ) + new MaterialApp( + home: new Center( + child: new Material( + child: new Stepper( + steps: [ + const Step( + title: const Text('A'), + state: StepState.complete, + content: const SizedBox( + width: 100.0, + height: 100.0, + ), + ), + const Step( + title: const Text('B'), + content: const SizedBox( + width: 100.0, + height: 100.0, + ), + ), + ], + ), + ), + ), + ), ); expect(find.text('1'), findsNothing); @@ -348,22 +370,24 @@ void main() { testWidgets('Stepper error test', (WidgetTester tester) async { await tester.pumpWidget( - new Center( - child: new Material( - child: new Stepper( - steps: [ - const Step( - title: const Text('A'), - state: StepState.error, - content: const SizedBox( - width: 100.0, - height: 100.0 - ) - ) - ] - ) - ) - ) + new MaterialApp( + home: new Center( + child: new Material( + child: new Stepper( + steps: [ + const Step( + title: const Text('A'), + state: StepState.error, + content: const SizedBox( + width: 100.0, + height: 100.0, + ), + ), + ], + ), + ), + ), + ), ); expect(find.text('!'), findsOneWidget); diff --git a/packages/flutter/test/material/tabs_test.dart b/packages/flutter/test/material/tabs_test.dart index b57b80289e..eb9a3b4dfc 100644 --- a/packages/flutter/test/material/tabs_test.dart +++ b/packages/flutter/test/material/tabs_test.dart @@ -14,6 +14,15 @@ import '../rendering/mock_canvas.dart'; import '../rendering/recording_canvas.dart'; import '../widgets/semantics_tester.dart'; +Widget boilerplate({ Widget child }) { + return new Directionality( + textDirection: TextDirection.ltr, + child: new Material( + child: child, + ), + ); +} + class StateMarker extends StatefulWidget { const StateMarker({ Key key, this.child }) : super(key: key); @@ -41,7 +50,7 @@ Widget buildFrame({ bool isScrollable: false, Color indicatorColor, }) { - return new Material( + return boilerplate( child: new DefaultTabController( initialIndex: tabs.indexOf(value), length: tabs.length, @@ -253,7 +262,7 @@ void main() { String value = tabs[0]; Widget builder() { - return new Material( + return boilerplate( child: new DefaultTabController( initialIndex: tabs.indexOf(value), length: tabs.length, @@ -633,7 +642,7 @@ void main() { Color secondColor; await tester.pumpWidget( - new Material( + boilerplate( child: new TabBar( controller: controller, labelColor: Colors.green[500], @@ -667,7 +676,7 @@ void main() { ); await tester.pumpWidget( - new Material( + boilerplate( child: new TabBarView( controller: controller, children: [ const Text('First'), const Text('Second') ], @@ -765,7 +774,7 @@ void main() { ); Widget buildFrame() { - return new Material( + return boilerplate( child: new TabBar( key: new UniqueKey(), controller: controller, @@ -893,7 +902,7 @@ void main() { ); await tester.pumpWidget( - new Material( + boilerplate( child: new TabBar( isScrollable: true, controller: controller, @@ -927,7 +936,7 @@ void main() { ); await tester.pumpWidget( - new Material( + boilerplate( child: new Column( children: [ new TabBar( @@ -985,7 +994,7 @@ void main() { ); await tester.pumpWidget( - new Material( + boilerplate( child: new Semantics( container: true, child: new TabBar( @@ -1034,7 +1043,7 @@ void main() { ); await tester.pumpWidget( - new Material( + boilerplate( child: new Column( children: [ new TabBar( @@ -1074,7 +1083,7 @@ void main() { ); await tester.pumpWidget( - new Material( + boilerplate( child: new Column( children: [ new TabBar( @@ -1129,7 +1138,7 @@ void main() { ); await tester.pumpWidget( - new Material( + boilerplate( child: new Column( children: [ new TabBar( diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart index 5c5f13e722..f33fa21788 100644 --- a/packages/flutter/test/material/text_field_test.dart +++ b/packages/flutter/test/material/text_field_test.dart @@ -27,15 +27,36 @@ class MockClipboard { } } -Widget overlay(Widget child) { - return new MediaQuery( - data: const MediaQueryData(size: const Size(800.0, 600.0)), - child: new Overlay( - initialEntries: [ - new OverlayEntry( - builder: (BuildContext context) => child, +Widget overlay({ Widget child }) { + return new Directionality( + textDirection: TextDirection.ltr, + child: new MediaQuery( + data: const MediaQueryData(size: const Size(800.0, 600.0)), + child: new Overlay( + initialEntries: [ + new OverlayEntry( + builder: (BuildContext context) => new Center( + child: new Material( + child: child, + ), + ), + ), + ], + ), + ), + ); +} + +Widget boilerplate({ Widget child }) { + return new Directionality( + textDirection: TextDirection.ltr, + child: new MediaQuery( + data: const MediaQueryData(size: const Size(800.0, 600.0)), + child: new Center( + child: new Material( + child: child, ), - ], + ), ), ); } @@ -100,23 +121,19 @@ void main() { final Key textFieldKey = new UniqueKey(); String textFieldValue; - Widget builder() { - return new Center( - child: new Material( - child: new TextField( - key: textFieldKey, - decoration: const InputDecoration( - hintText: 'Placeholder', - ), - onChanged: (String value) { - textFieldValue = value; - } + await tester.pumpWidget( + overlay( + child: new TextField( + key: textFieldKey, + decoration: const InputDecoration( + hintText: 'Placeholder', ), + onChanged: (String value) { + textFieldValue = value; + } ), - ); - } - - await tester.pumpWidget(builder()); + ) + ); RenderBox findTextFieldBox() => tester.renderObject(find.byKey(textFieldKey)); @@ -126,11 +143,8 @@ void main() { Future checkText(String testValue) async { return TestAsyncUtils.guard(() async { await tester.enterText(find.byType(TextField), testValue); - // Check that the onChanged event handler fired. expect(textFieldValue, equals(testValue)); - - await tester.pumpWidget(builder()); await skipPastScrollingAnimation(tester); }); } @@ -146,20 +160,15 @@ void main() { }); testWidgets('Cursor blinks', (WidgetTester tester) async { - - Widget builder() { - return const Center( - child: const Material( - child: const TextField( - decoration: const InputDecoration( - hintText: 'Placeholder', - ), + await tester.pumpWidget( + overlay( + child: const TextField( + decoration: const InputDecoration( + hintText: 'Placeholder', ), ), - ); - } - - await tester.pumpWidget(builder()); + ), + ); await tester.showKeyboard(find.byType(TextField)); final EditableTextState editableText = tester.state(find.byType(EditableText)); @@ -191,20 +200,16 @@ void main() { }); testWidgets('obscureText control test', (WidgetTester tester) async { - Widget builder() { - return const Center( - child: const Material( - child: const TextField( - obscureText: true, - decoration: const InputDecoration( - hintText: 'Placeholder', - ), + await tester.pumpWidget( + overlay( + child: const TextField( + obscureText: true, + decoration: const InputDecoration( + hintText: 'Placeholder', ), ), - ); - } - - await tester.pumpWidget(builder()); + ), + ); await tester.showKeyboard(find.byType(TextField)); const String testValue = 'ABC'; @@ -219,24 +224,18 @@ void main() { testWidgets('Caret position is updated on tap', (WidgetTester tester) async { final TextEditingController controller = new TextEditingController(); - Widget builder() { - return overlay(new Center( - child: new Material( - child: new TextField( - controller: controller, - ), + await tester.pumpWidget( + overlay( + child: new TextField( + controller: controller, ), - )); - } - - await tester.pumpWidget(builder()); + ) + ); expect(controller.selection.baseOffset, -1); expect(controller.selection.extentOffset, -1); final String testValue = 'abc def ghi'; await tester.enterText(find.byType(TextField), testValue); - - await tester.pumpWidget(builder()); await skipPastScrollingAnimation(tester); // Tap to reposition the caret. @@ -252,34 +251,17 @@ void main() { testWidgets('Can long press to select', (WidgetTester tester) async { final TextEditingController controller = new TextEditingController(); - Widget builder() { - return new MediaQuery( - data: const MediaQueryData(), - child: new Overlay( - initialEntries: [ - new OverlayEntry( - builder: (BuildContext context) { - return new Center( - child: new Material( - child: new TextField( - controller: controller, - ), - ), - ); - }, - ), - ], + await tester.pumpWidget( + overlay( + child: new TextField( + controller: controller, ), - ); - } - - await tester.pumpWidget(builder()); + ) + ); final String testValue = 'abc def ghi'; await tester.enterText(find.byType(TextField), testValue); expect(controller.value.text, testValue); - - await tester.pumpWidget(builder()); await skipPastScrollingAnimation(tester); expect(controller.selection.isCollapsed, true); @@ -299,22 +281,16 @@ void main() { testWidgets('Can drag handles to change selection', (WidgetTester tester) async { final TextEditingController controller = new TextEditingController(); - Widget builder() { - return overlay(new Center( - child: new Material( - child: new TextField( - controller: controller, - ), + await tester.pumpWidget( + overlay( + child: new TextField( + controller: controller, ), - )); - } - - await tester.pumpWidget(builder()); + ), + ); final String testValue = 'abc def ghi'; await tester.enterText(find.byType(TextField), testValue); - - await tester.pumpWidget(builder()); await skipPastScrollingAnimation(tester); // Long press the 'e' to select 'def'. @@ -344,7 +320,7 @@ void main() { await gesture.moveTo(newHandlePos); await tester.pump(); await gesture.up(); - await tester.pumpWidget(builder()); + await tester.pump(); expect(controller.selection.baseOffset, selection.baseOffset); expect(controller.selection.extentOffset, selection.extentOffset+2); @@ -357,7 +333,7 @@ void main() { await gesture.moveTo(newHandlePos); await tester.pump(); await gesture.up(); - await tester.pumpWidget(builder()); + await tester.pump(); expect(controller.selection.baseOffset, selection.baseOffset-2); expect(controller.selection.extentOffset, selection.extentOffset+2); @@ -366,26 +342,21 @@ void main() { testWidgets('Can use selection toolbar', (WidgetTester tester) async { final TextEditingController controller = new TextEditingController(); - Widget builder() { - return overlay(new Center( - child: new Material( - child: new TextField( - controller: controller, - ), + await tester.pumpWidget( + overlay( + child: new TextField( + controller: controller, ), - )); - } - - await tester.pumpWidget(builder()); + ), + ); final String testValue = 'abc def ghi'; await tester.enterText(find.byType(TextField), testValue); - await tester.pumpWidget(builder()); await skipPastScrollingAnimation(tester); // Tap the selection handle to bring up the "paste / select all" menu. await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e'))); - await tester.pumpWidget(builder()); + await tester.pump(); await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero RenderEditable renderEditable = findRenderEditable(tester); List endpoints = globalize( @@ -393,24 +364,23 @@ void main() { renderEditable, ); await tester.tapAt(endpoints[0].point + const Offset(1.0, 1.0)); - await tester.pumpWidget(builder()); + await tester.pump(); await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero // SELECT ALL should select all the text. await tester.tap(find.text('SELECT ALL')); - await tester.pumpWidget(builder()); + await tester.pump(); expect(controller.selection.baseOffset, 0); expect(controller.selection.extentOffset, testValue.length); // COPY should reset the selection. await tester.tap(find.text('COPY')); - await tester.pumpWidget(builder()); await skipPastScrollingAnimation(tester); expect(controller.selection.isCollapsed, true); // Tap again to bring back the menu. await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e'))); - await tester.pumpWidget(builder()); + await tester.pump(); await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero renderEditable = findRenderEditable(tester); endpoints = globalize( @@ -418,38 +388,33 @@ void main() { renderEditable, ); await tester.tapAt(endpoints[0].point + const Offset(1.0, 1.0)); - await tester.pumpWidget(builder()); + await tester.pump(); await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero // PASTE right before the 'e'. await tester.tap(find.text('PASTE')); - await tester.pumpWidget(builder()); + await tester.pump(); expect(controller.text, 'abc d${testValue}ef ghi'); }); testWidgets('Selection toolbar fades in', (WidgetTester tester) async { final TextEditingController controller = new TextEditingController(); - Widget builder() { - return overlay(new Center( - child: new Material( - child: new TextField( - controller: controller, - ), + await tester.pumpWidget( + overlay( + child: new TextField( + controller: controller, ), - )); - } - - await tester.pumpWidget(builder()); + ), + ); final String testValue = 'abc def ghi'; await tester.enterText(find.byType(TextField), testValue); - await tester.pumpWidget(builder()); await skipPastScrollingAnimation(tester); // Tap the selection handle to bring up the "paste / select all" menu. await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e'))); - await tester.pumpWidget(builder()); + await tester.pump(); await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero final RenderEditable renderEditable = findRenderEditable(tester); final List endpoints = globalize( @@ -457,7 +422,7 @@ void main() { renderEditable, ); await tester.tapAt(endpoints[0].point + const Offset(1.0, 1.0)); - await tester.pumpWidget(builder()); + await tester.pump(); // Toolbar should fade in. Starting at 0% opacity. final Element target = tester.element(find.text('SELECT ALL')); @@ -478,15 +443,13 @@ void main() { final Key textFieldKey = new UniqueKey(); Widget builder(int maxLines) { - return new Center( - child: new Material( - child: new TextField( - key: textFieldKey, - style: const TextStyle(color: Colors.black, fontSize: 34.0), - maxLines: maxLines, - decoration: const InputDecoration( - hintText: 'Placeholder', - ), + return boilerplate( + child: new TextField( + key: textFieldKey, + style: const TextStyle(color: Colors.black, fontSize: 34.0), + maxLines: maxLines, + decoration: const InputDecoration( + hintText: 'Placeholder', ), ), ); @@ -543,25 +506,19 @@ void main() { testWidgets('Can drag handles to change selection in multiline', (WidgetTester tester) async { final TextEditingController controller = new TextEditingController(); - Widget builder() { - return overlay(new Center( - child: new Material( - child: new TextField( - controller: controller, - style: const TextStyle(color: Colors.black, fontSize: 34.0), - maxLines: 3, - ), + await tester.pumpWidget( + overlay( + child: new TextField( + controller: controller, + style: const TextStyle(color: Colors.black, fontSize: 34.0), + maxLines: 3, ), - )); - } - - await tester.pumpWidget(builder()); + ), + ); final String testValue = kThreeLines; final String cutValue = 'First line of stuff '; await tester.enterText(find.byType(TextField), testValue); - - await tester.pumpWidget(builder()); await skipPastScrollingAnimation(tester); // Check that the text spans multiple lines. @@ -599,7 +556,7 @@ void main() { await gesture.moveTo(newHandlePos); await tester.pump(); await gesture.up(); - await tester.pumpWidget(builder()); + await tester.pump(); expect(controller.selection.baseOffset, 39); expect(controller.selection.extentOffset, 50); @@ -612,13 +569,13 @@ void main() { await gesture.moveTo(newHandlePos); await tester.pump(); await gesture.up(); - await tester.pumpWidget(builder()); + await tester.pump(); expect(controller.selection.baseOffset, 5); expect(controller.selection.extentOffset, 50); await tester.tap(find.text('CUT')); - await tester.pumpWidget(builder()); + await tester.pump(); expect(controller.selection.isCollapsed, true); expect(controller.text, cutValue); }); @@ -627,25 +584,21 @@ void main() { final Key textFieldKey = new UniqueKey(); final TextEditingController controller = new TextEditingController(); - Widget builder() { - return overlay(new Center( - child: new Material( - child: new TextField( - key: textFieldKey, - controller: controller, - style: const TextStyle(color: Colors.black, fontSize: 34.0), - maxLines: 2, - ), + await tester.pumpWidget( + overlay( + child: new TextField( + key: textFieldKey, + controller: controller, + style: const TextStyle(color: Colors.black, fontSize: 34.0), + maxLines: 2, ), - )); - } - - await tester.pumpWidget(builder()); + ), + ); await tester.pump(const Duration(seconds: 1)); await tester.enterText(find.byType(TextField), kMoreThanFourLines); - await tester.pumpWidget(builder()); + await tester.pump(); await tester.pump(const Duration(seconds: 1)); RenderBox findInputBox() => tester.renderObject(find.byKey(textFieldKey)); @@ -717,20 +670,16 @@ void main() { testWidgets('TextField smoke test', (WidgetTester tester) async { String textFieldValue; - Widget builder() { - return new Center( - child: new Material( - child: new TextField( - decoration: null, - onChanged: (String value) { - textFieldValue = value; - }, - ), + await tester.pumpWidget( + overlay( + child: new TextField( + decoration: null, + onChanged: (String value) { + textFieldValue = value; + }, ), - ); - } - - await tester.pumpWidget(builder()); + ), + ); Future checkText(String testValue) { return TestAsyncUtils.guard(() async { @@ -739,7 +688,7 @@ void main() { // Check that the onChanged event handler fired. expect(textFieldValue, equals(testValue)); - await tester.pumpWidget(builder()); + await tester.pump(); }); } @@ -750,21 +699,17 @@ void main() { final GlobalKey textFieldKey = new GlobalKey(debugLabel: 'textFieldKey'); String textFieldValue; - Widget builder() { - return new Center( - child: new Material( - child: new TextField( - key: textFieldKey, - decoration: const InputDecoration( - hintText: 'Placeholder', - ), - onChanged: (String value) { textFieldValue = value; }, + await tester.pumpWidget( + overlay( + child: new TextField( + key: textFieldKey, + decoration: const InputDecoration( + hintText: 'Placeholder', ), + onChanged: (String value) { textFieldValue = value; }, ), - ); - } - - await tester.pumpWidget(builder()); + ), + ); Future checkText(String testValue) async { return TestAsyncUtils.guard(() async { @@ -773,7 +718,7 @@ void main() { // Check that the onChanged event handler fired. expect(textFieldValue, equals(testValue)); - await tester.pumpWidget(builder()); + await tester.pump(); }); } @@ -781,20 +726,16 @@ void main() { }); testWidgets('TextField errorText trumps helperText', (WidgetTester tester) async { - Widget builder() { - return const Center( - child: const Material( - child: const TextField( - decoration: const InputDecoration( - errorText: 'error text', - helperText: 'helper text', - ), + await tester.pumpWidget( + overlay( + child: const TextField( + decoration: const InputDecoration( + errorText: 'error text', + helperText: 'helper text', ), ), - ); - } - - await tester.pumpWidget(builder()); + ), + ); expect(find.text('helper text'), findsNothing); expect(find.text('error text'), findsOneWidget); }); @@ -804,22 +745,18 @@ void main() { hintColor: Colors.blue[500], ); - Widget builder() { - return new Center( + await tester.pumpWidget( + overlay( child: new Theme( data: themeData, - child: const Material( - child: const TextField( - decoration: const InputDecoration( - helperText: 'helper text', - ), + child: const TextField( + decoration: const InputDecoration( + helperText: 'helper text', ), ), ), - ); - } - - await tester.pumpWidget(builder()); + ), + ); final Text helperText = tester.widget(find.text('helper text')); expect(helperText.style.color, themeData.hintColor); expect(helperText.style.fontSize, themeData.textTheme.caption.fontSize); @@ -831,20 +768,16 @@ void main() { fontSize: 10.0, ); - Widget builder() { - return new Center( - child: new Material( - child: new TextField( - decoration: new InputDecoration( - helperText: 'helper text', - helperStyle: style, - ), + await tester.pumpWidget( + overlay( + child: new TextField( + decoration: new InputDecoration( + helperText: 'helper text', + helperStyle: style, ), ), - ); - } - - await tester.pumpWidget(builder()); + ), + ); final Text helperText = tester.widget(find.text('helper text')); expect(helperText.style, style); }); @@ -858,23 +791,19 @@ void main() { hintColor: Colors.blue[500], ); - Widget builder() { - return new Center( + await tester.pumpWidget( + overlay( child: new Theme( data: themeData, - child: new Material( - child: new TextField( - decoration: const InputDecoration( - hintText: 'Placeholder', - ), - style: style, + child: new TextField( + decoration: const InputDecoration( + hintText: 'Placeholder', ), + style: style, ), ), - ); - } - - await tester.pumpWidget(builder()); + ), + ); final Text hintText = tester.widget(find.text('Placeholder')); expect(hintText.style.color, themeData.hintColor); @@ -887,20 +816,16 @@ void main() { fontSize: 10.0, ); - Widget builder() { - return new Center( - child: new Material( - child: new TextField( - decoration: new InputDecoration( - hintText: 'Placeholder', - hintStyle: hintStyle, - ), + await tester.pumpWidget( + overlay( + child: new TextField( + decoration: new InputDecoration( + hintText: 'Placeholder', + hintStyle: hintStyle, ), ), - ); - } - - await tester.pumpWidget(builder()); + ), + ); final Text hintText = tester.widget(find.text('Placeholder')); expect(hintText.style, hintStyle); @@ -912,20 +837,16 @@ void main() { fontSize: 10.0, ); - Widget builder() { - return new Center( - child: new Material( - child: new TextField( - decoration: new InputDecoration( - prefixText: 'Prefix:', - prefixStyle: prefixStyle, - ), + await tester.pumpWidget( + overlay( + child: new TextField( + decoration: new InputDecoration( + prefixText: 'Prefix:', + prefixStyle: prefixStyle, ), ), - ); - } - - await tester.pumpWidget(builder()); + ), + ); final Text prefixText = tester.widget(find.text('Prefix:')); expect(prefixText.style, prefixStyle); @@ -937,20 +858,16 @@ void main() { fontSize: 10.0, ); - Widget builder() { - return new Center( - child: new Material( - child: new TextField( - decoration: new InputDecoration( - suffixText: '.com', - suffixStyle: suffixStyle, - ), + await tester.pumpWidget( + overlay( + child: new TextField( + decoration: new InputDecoration( + suffixText: '.com', + suffixStyle: suffixStyle, ), ), - ); - } - - await tester.pumpWidget(builder()); + ), + ); final Text suffixText = tester.widget(find.text('.com')); expect(suffixText.style, suffixStyle); @@ -960,31 +877,26 @@ void main() { (WidgetTester tester) async { final Key secondKey = new UniqueKey(); - Widget innerBuilder() { - return new Center( - child: new Material( - child: new Column( - children: [ - const TextField( - decoration: const InputDecoration( - labelText: 'First', - ), + await tester.pumpWidget( + overlay( + child: new Column( + children: [ + const TextField( + decoration: const InputDecoration( + labelText: 'First', ), - new TextField( - key: secondKey, - decoration: const InputDecoration( - prefixText: 'Prefix', - suffixText: 'Suffix', - ), + ), + new TextField( + key: secondKey, + decoration: const InputDecoration( + prefixText: 'Prefix', + suffixText: 'Suffix', ), - ], - ), + ), + ], ), - ); - } - Widget builder() => overlay(innerBuilder()); - - await tester.pumpWidget(builder()); + ), + ); expect(find.text('Prefix'), findsOneWidget); expect(find.text('Suffix'), findsOneWidget); @@ -1013,33 +925,28 @@ void main() { ); final Key secondKey = new UniqueKey(); - Widget innerBuilder() { - return new Center( - child: new Material( - child: new Column( - children: [ - const TextField( - decoration: const InputDecoration( - labelText: 'First', - ), + await tester.pumpWidget( + overlay( + child: new Column( + children: [ + const TextField( + decoration: const InputDecoration( + labelText: 'First', ), - new TextField( - key: secondKey, - decoration: new InputDecoration( - hintText: 'Hint', - hintStyle: hintStyle, - prefixText: 'Prefix', - suffixText: 'Suffix', - ), + ), + new TextField( + key: secondKey, + decoration: new InputDecoration( + hintText: 'Hint', + hintStyle: hintStyle, + prefixText: 'Prefix', + suffixText: 'Suffix', ), - ], - ), + ), + ], ), - ); - } - Widget builder() => overlay(innerBuilder()); - - await tester.pumpWidget(builder()); + ), + ); // Neither the prefix or the suffix should initially be visible, only the hint. expect(find.text('Prefix'), findsNothing); @@ -1089,34 +996,29 @@ void main() { ); final Key secondKey = new UniqueKey(); - Widget innerBuilder() { - return new Center( - child: new Material( - child: new Column( - children: [ - const TextField( - decoration: const InputDecoration( - labelText: 'First', - ), + await tester.pumpWidget( + overlay( + child: new Column( + children: [ + const TextField( + decoration: const InputDecoration( + labelText: 'First', ), - new TextField( - key: secondKey, - decoration: new InputDecoration( - labelText: 'Label', - prefixText: 'Prefix', - prefixStyle: prefixStyle, - suffixText: 'Suffix', - suffixStyle: suffixStyle, - ), + ), + new TextField( + key: secondKey, + decoration: new InputDecoration( + labelText: 'Label', + prefixText: 'Prefix', + prefixStyle: prefixStyle, + suffixText: 'Suffix', + suffixStyle: suffixStyle, ), - ], - ), + ), + ], ), - ); - } - Widget builder() => overlay(innerBuilder()); - - await tester.pumpWidget(builder()); + ), + ); // Not focused. The prefix should not display, but the label should. expect(find.text('Prefix'), findsNothing); @@ -1151,30 +1053,25 @@ void main() { testWidgets('TextField label text animates', (WidgetTester tester) async { final Key secondKey = new UniqueKey(); - Widget innerBuilder() { - return new Center( - child: new Material( - child: new Column( - children: [ - const TextField( - decoration: const InputDecoration( - labelText: 'First', - ), + await tester.pumpWidget( + overlay( + child: new Column( + children: [ + const TextField( + decoration: const InputDecoration( + labelText: 'First', ), - new TextField( - key: secondKey, - decoration: const InputDecoration( - labelText: 'Second', - ), + ), + new TextField( + key: secondKey, + decoration: const InputDecoration( + labelText: 'Second', ), - ], - ), + ), + ], ), - ); - } - Widget builder() => overlay(innerBuilder()); - - await tester.pumpWidget(builder()); + ), + ); Offset pos = tester.getTopLeft(find.text('Second')); @@ -1196,13 +1093,11 @@ void main() { testWidgets('No space between Input icon and text', (WidgetTester tester) async { await tester.pumpWidget( - const Center( - child: const Material( - child: const TextField( - decoration: const InputDecoration( - icon: const Icon(Icons.phone), - labelText: 'label', - ), + overlay( + child: const TextField( + decoration: const InputDecoration( + icon: const Icon(Icons.phone), + labelText: 'label', ), ), ), @@ -1215,15 +1110,13 @@ void main() { testWidgets('Collapsed hint text placement', (WidgetTester tester) async { await tester.pumpWidget( - overlay(const Center( - child: const Material( - child: const TextField( - decoration: const InputDecoration.collapsed( - hintText: 'hint', - ), + overlay( + child: const TextField( + decoration: const InputDecoration.collapsed( + hintText: 'hint', ), ), - )), + ), ); expect(tester.getTopLeft(find.text('hint')), equals(tester.getTopLeft(find.byType(TextField)))); @@ -1231,17 +1124,15 @@ void main() { testWidgets('Can align to center', (WidgetTester tester) async { await tester.pumpWidget( - overlay(new Center( - child: new Material( - child: new Container( - width: 300.0, - child: const TextField( - textAlign: TextAlign.center, - decoration: null, - ), + overlay( + child: new Container( + width: 300.0, + child: const TextField( + textAlign: TextAlign.center, + decoration: null, ), ), - )), + ), ); final RenderEditable editable = findRenderEditable(tester); @@ -1263,19 +1154,17 @@ void main() { testWidgets('Can align to center within center', (WidgetTester tester) async { await tester.pumpWidget( - overlay(new Center( - child: new Material( - child: new Container( - width: 300.0, - child: const Center( - child: const TextField( - textAlign: TextAlign.center, - decoration: null, - ), + overlay( + child: new Container( + width: 300.0, + child: const Center( + child: const TextField( + textAlign: TextAlign.center, + decoration: null, ), ), ), - )), + ), ); final RenderEditable editable = findRenderEditable(tester); @@ -1307,17 +1196,15 @@ void main() { StateSetter setState; await tester.pumpWidget( - overlay(new Center( - child: new Material( - child: new StatefulBuilder( - builder: (BuildContext context, StateSetter setter) { - setState = setter; - return new TextField(controller: currentController); - } - ), + overlay( + child: new StatefulBuilder( + builder: (BuildContext context, StateSetter setter) { + setState = setter; + return new TextField(controller: currentController); + } ), ), - )); + ); expect(tester.testTextInput.editingState['text'], isEmpty); await tester.tap(find.byType(TextField)); @@ -1352,178 +1239,149 @@ void main() { expect(tester.testTextInput.editingState['text'], equals('')); }); - testWidgets( - 'Cannot enter new lines onto single line TextField', - (WidgetTester tester) async { - final TextEditingController textController = new TextEditingController(); + testWidgets('Cannot enter new lines onto single line TextField', (WidgetTester tester) async { + final TextEditingController textController = new TextEditingController(); - await tester.pumpWidget(new Material( - child: new TextField(controller: textController, decoration: null), - )); + await tester.pumpWidget(new Material( + child: new TextField(controller: textController, decoration: null), + )); - await tester.enterText(find.byType(TextField), 'abc\ndef'); + await tester.enterText(find.byType(TextField), 'abc\ndef'); - expect(textController.text, 'abcdef'); - } - ); + expect(textController.text, 'abcdef'); + }); - testWidgets( - 'Injected formatters are chained', - (WidgetTester tester) async { - final TextEditingController textController = new TextEditingController(); + testWidgets('Injected formatters are chained', (WidgetTester tester) async { + final TextEditingController textController = new TextEditingController(); - await tester.pumpWidget(new Material( + await tester.pumpWidget(new Material( + child: new TextField( + controller: textController, + decoration: null, + inputFormatters: [ + new BlacklistingTextInputFormatter( + new RegExp(r'[a-z]'), + replacementString: '#', + ), + ], + ), + )); + + await tester.enterText(find.byType(TextField), 'a一b二c三\nd四e五f六'); + // The default single line formatter replaces \n with empty string. + expect(textController.text, '#一#二#三#四#五#六'); + }); + + testWidgets('Chained formatters are in sequence', (WidgetTester tester) async { + final TextEditingController textController = new TextEditingController(); + + await tester.pumpWidget(new Material( + child: new TextField( + controller: textController, + decoration: null, + maxLines: 2, + inputFormatters: [ + new BlacklistingTextInputFormatter( + new RegExp(r'[a-z]'), + replacementString: '12\n', + ), + new WhitelistingTextInputFormatter(new RegExp(r'\n[0-9]')), + ], + ), + )); + + await tester.enterText(find.byType(TextField), 'a1b2c3'); + // The first formatter turns it into + // 12\n112\n212\n3 + // The second formatter turns it into + // \n1\n2\n3 + // Multiline is allowed since maxLine != 1. + expect(textController.text, '\n1\n2\n3'); + }); + + testWidgets('Pasted values are formatted', (WidgetTester tester) async { + final TextEditingController textController = new TextEditingController(); + + await tester.pumpWidget( + overlay( child: new TextField( controller: textController, decoration: null, inputFormatters: [ - new BlacklistingTextInputFormatter( - new RegExp(r'[a-z]'), - replacementString: '#', - ), + WhitelistingTextInputFormatter.digitsOnly, ], ), - )); + ), + ); - await tester.enterText(find.byType(TextField), 'a一b二c三\nd四e五f六'); - // The default single line formatter replaces \n with empty string. - expect(textController.text, '#一#二#三#四#五#六'); - } - ); + await tester.enterText(find.byType(TextField), 'a1b\n2c3'); + expect(textController.text, '123'); + await skipPastScrollingAnimation(tester); - testWidgets( - 'Chained formatters are in sequence', - (WidgetTester tester) async { - final TextEditingController textController = new TextEditingController(); + await tester.tapAt(textOffsetToPosition(tester, '123'.indexOf('2'))); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero + final RenderEditable renderEditable = findRenderEditable(tester); + final List endpoints = globalize( + renderEditable.getEndpointsForSelection(textController.selection), + renderEditable, + ); + await tester.tapAt(endpoints[0].point + const Offset(1.0, 1.0)); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero - await tester.pumpWidget(new Material( - child: new TextField( - controller: textController, - decoration: null, - maxLines: 2, - inputFormatters: [ - new BlacklistingTextInputFormatter( - new RegExp(r'[a-z]'), - replacementString: '12\n', - ), - new WhitelistingTextInputFormatter(new RegExp(r'\n[0-9]')), - ], + Clipboard.setData(const ClipboardData(text: '一4二\n5三6')); + await tester.tap(find.text('PASTE')); + await tester.pump(); + // Puts 456 before the 2 in 123. + expect(textController.text, '145623'); + }); + + testWidgets('Text field scrolls the caret into view', (WidgetTester tester) async { + final TextEditingController controller = new TextEditingController(); + + await tester.pumpWidget( + overlay( + child: new Container( + width: 100.0, + child: new TextField( + controller: controller, + ), ), - )); + ), + ); - await tester.enterText(find.byType(TextField), 'a1b2c3'); - // The first formatter turns it into - // 12\n112\n212\n3 - // The second formatter turns it into - // \n1\n2\n3 - // Multiline is allowed since maxLine != 1. - expect(textController.text, '\n1\n2\n3'); - } - ); + final String longText = 'a' * 20; + await tester.enterText(find.byType(TextField), longText); + await skipPastScrollingAnimation(tester); - testWidgets( - 'Pasted values are formatted', - (WidgetTester tester) async { - final TextEditingController textController = new TextEditingController(); + ScrollableState scrollableState = tester.firstState(find.byType(Scrollable)); + expect(scrollableState.position.pixels, equals(0.0)); - Widget builder() { - return overlay(new Center( - child: new Material( - child: new TextField( - controller: textController, - decoration: null, - inputFormatters: [ - WhitelistingTextInputFormatter.digitsOnly, - ], - ), - ), - )); - } + // Move the caret to the end of the text and check that the text field + // scrolls to make the caret visible. + controller.selection = new TextSelection.collapsed(offset: longText.length); + await tester.pump(); // TODO(ianh): Figure out why this extra pump is needed. + await skipPastScrollingAnimation(tester); - await tester.pumpWidget(builder()); - - await tester.enterText(find.byType(TextField), 'a1b\n2c3'); - expect(textController.text, '123'); - await tester.pumpWidget(builder()); - await skipPastScrollingAnimation(tester); - - await tester.tapAt(textOffsetToPosition(tester, '123'.indexOf('2'))); - await tester.pumpWidget(builder()); - await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero - final RenderEditable renderEditable = findRenderEditable(tester); - final List endpoints = globalize( - renderEditable.getEndpointsForSelection(textController.selection), - renderEditable, - ); - await tester.tapAt(endpoints[0].point + const Offset(1.0, 1.0)); - await tester.pumpWidget(builder()); - await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero - - Clipboard.setData(const ClipboardData(text: '一4二\n5三6')); - await tester.tap(find.text('PASTE')); - await tester.pumpWidget(builder()); - // Puts 456 before the 2 in 123. - expect(textController.text, '145623'); - } - ); - - testWidgets( - 'Text field scrolls the caret into view', - (WidgetTester tester) async { - final TextEditingController controller = new TextEditingController(); - - Widget builder() { - return overlay(new Center( - child: new Material( - child: new Container( - width: 100.0, - child: new TextField( - controller: controller, - ), - ), - ), - )); - } - - await tester.pumpWidget(builder()); - - final String longText = 'a' * 20; - await tester.enterText(find.byType(TextField), longText); - await tester.pumpWidget(builder()); - await skipPastScrollingAnimation(tester); - - ScrollableState scrollableState = tester.firstState(find.byType(Scrollable)); - expect(scrollableState.position.pixels, equals(0.0)); - - // Move the caret to the end of the text and check that the text field - // scrolls to make the caret visible. - controller.selection = new TextSelection.collapsed(offset: longText.length); - await tester.pumpWidget(builder()); - await skipPastScrollingAnimation(tester); - - scrollableState = tester.firstState(find.byType(Scrollable)); - expect(scrollableState.position.pixels, isNot(equals(0.0))); - } - ); + scrollableState = tester.firstState(find.byType(Scrollable)); + expect(scrollableState.position.pixels, isNot(equals(0.0))); + }); testWidgets('haptic feedback', (WidgetTester tester) async { final FeedbackTester feedback = new FeedbackTester(); final TextEditingController controller = new TextEditingController(); - Widget builder() { - return overlay(new Center( - child: new Material( - child: new Container( - width: 100.0, - child: new TextField( - controller: controller, - ), + await tester.pumpWidget( + overlay( + child: new Container( + width: 100.0, + child: new TextField( + controller: controller, ), ), - )); - } - - await tester.pumpWidget(builder()); + ), + ); await tester.tap(find.byType(TextField)); await tester.pumpAndSettle(const Duration(seconds: 1)); @@ -1538,58 +1396,49 @@ void main() { feedback.dispose(); }); - testWidgets( - 'Text field drops selection when losing focus', - (WidgetTester tester) async { - final Key key1 = new UniqueKey(); - final TextEditingController controller1 = new TextEditingController(); - final Key key2 = new UniqueKey(); + testWidgets('Text field drops selection when losing focus', (WidgetTester tester) async { + final Key key1 = new UniqueKey(); + final TextEditingController controller1 = new TextEditingController(); + final Key key2 = new UniqueKey(); - Widget builder() { - return overlay(new Center( - child: new Material( - child: new Column( - children: [ - new TextField( - key: key1, - controller: controller1 - ), - new TextField(key: key2), - ], + await tester.pumpWidget( + overlay( + child: new Column( + children: [ + new TextField( + key: key1, + controller: controller1 ), - ), - )); - } + new TextField(key: key2), + ], + ), + ), + ); - await tester.pumpWidget(builder()); - await tester.tap(find.byKey(key1)); - await tester.enterText(find.byKey(key1), 'abcd'); - await tester.pump(); - controller1.selection = const TextSelection(baseOffset: 0, extentOffset: 3); - await tester.pump(); - expect(controller1.selection, isNot(equals(TextRange.empty))); + await tester.tap(find.byKey(key1)); + await tester.enterText(find.byKey(key1), 'abcd'); + await tester.pump(); + controller1.selection = const TextSelection(baseOffset: 0, extentOffset: 3); + await tester.pump(); + expect(controller1.selection, isNot(equals(TextRange.empty))); - await tester.tap(find.byKey(key2)); - await tester.pump(); - expect(controller1.selection, equals(TextRange.empty)); - } - ); + await tester.tap(find.byKey(key2)); + await tester.pump(); + expect(controller1.selection, equals(TextRange.empty)); + }); - testWidgets( - 'Selection is consistent with text length', - (WidgetTester tester) async { - final TextEditingController controller = new TextEditingController(); + testWidgets('Selection is consistent with text length', (WidgetTester tester) async { + final TextEditingController controller = new TextEditingController(); - controller.text = 'abcde'; - controller.selection = const TextSelection.collapsed(offset: 5); + controller.text = 'abcde'; + controller.selection = const TextSelection.collapsed(offset: 5); - controller.text = ''; - expect(controller.selection.start, lessThanOrEqualTo(0)); - expect(controller.selection.end, lessThanOrEqualTo(0)); + controller.text = ''; + expect(controller.selection.start, lessThanOrEqualTo(0)); + expect(controller.selection.end, lessThanOrEqualTo(0)); - expect(() { - controller.selection = const TextSelection.collapsed(offset: 10); - }, throwsFlutterError); - } - ); + expect(() { + controller.selection = const TextSelection.collapsed(offset: 10); + }, throwsFlutterError); + }); } diff --git a/packages/flutter/test/painting/box_painter_test.dart b/packages/flutter/test/painting/box_painter_test.dart index 672f0333be..f1d901dc63 100644 --- a/packages/flutter/test/painting/box_painter_test.dart +++ b/packages/flutter/test/painting/box_painter_test.dart @@ -167,7 +167,7 @@ void main() { ], ).toString(), equals( - 'LinearGradient(FractionalOffset(0.0, 0.0), FractionalOffset(0.0, 1.0), [Color(0x33333333), Color(0x66666666)], null, TileMode.clamp)', + 'LinearGradient(FractionalOffset.topLeft, FractionalOffset.bottomLeft, [Color(0x33333333), Color(0x66666666)], null, TileMode.clamp)', ), ); }); diff --git a/packages/flutter/test/painting/edge_insets_test.dart b/packages/flutter/test/painting/edge_insets_test.dart index f84a9959d6..76baaa3051 100644 --- a/packages/flutter/test/painting/edge_insets_test.dart +++ b/packages/flutter/test/painting/edge_insets_test.dart @@ -50,4 +50,141 @@ void main() { expect(EdgeInsets.lerp(null, b, 0.25), equals(b * 0.25)); expect(EdgeInsets.lerp(a, null, 0.25), equals(a * 0.75)); }); + + test('EdgeInsets.resolve()', () { + expect(new EdgeInsetsDirectional.fromSTEB(10.0, 20.0, 30.0, 40.0).resolve(TextDirection.ltr), const EdgeInsets.fromLTRB(10.0, 20.0, 30.0, 40.0)); + expect(new EdgeInsetsDirectional.fromSTEB(99.0, 98.0, 97.0, 96.0).resolve(TextDirection.rtl), const EdgeInsets.fromLTRB(97.0, 98.0, 99.0, 96.0)); + expect(new EdgeInsetsDirectional.only(start: 963.25).resolve(TextDirection.ltr), const EdgeInsets.fromLTRB(963.25, 0.0, 0.0, 0.0)); + expect(new EdgeInsetsDirectional.only(top: 963.25).resolve(TextDirection.ltr), const EdgeInsets.fromLTRB(0.0, 963.25, 0.0, 0.0)); + expect(new EdgeInsetsDirectional.only(end: 963.25).resolve(TextDirection.ltr), const EdgeInsets.fromLTRB(0.0, 0.0, 963.25, 0.0)); + expect(new EdgeInsetsDirectional.only(bottom: 963.25).resolve(TextDirection.ltr), const EdgeInsets.fromLTRB(0.0, 0.0, 0.0, 963.25)); + expect(new EdgeInsetsDirectional.only(start: 963.25).resolve(TextDirection.rtl), const EdgeInsets.fromLTRB(0.0, 0.0, 963.25, 0.0)); + expect(new EdgeInsetsDirectional.only(top: 963.25).resolve(TextDirection.rtl), const EdgeInsets.fromLTRB(0.0, 963.25, 0.0, 0.0)); + expect(new EdgeInsetsDirectional.only(end: 963.25).resolve(TextDirection.rtl), const EdgeInsets.fromLTRB(963.25, 0.0, 0.0, 0.0)); + expect(new EdgeInsetsDirectional.only(bottom: 963.25).resolve(TextDirection.rtl), const EdgeInsets.fromLTRB(0.0, 0.0, 0.0, 963.25)); + expect(new EdgeInsetsDirectional.only(), new EdgeInsetsDirectional.only()); + expect(new EdgeInsetsDirectional.only(top: 1.0).hashCode, isNot(new EdgeInsetsDirectional.only(bottom: 1.0))); + expect(new EdgeInsetsDirectional.fromSTEB(10.0, 20.0, 30.0, 40.0).resolve(TextDirection.ltr), + new EdgeInsetsDirectional.fromSTEB(30.0, 20.0, 10.0, 40.0).resolve(TextDirection.rtl)); + expect(new EdgeInsetsDirectional.fromSTEB(10.0, 20.0, 30.0, 40.0).resolve(TextDirection.ltr), + isNot(new EdgeInsetsDirectional.fromSTEB(30.0, 20.0, 10.0, 40.0).resolve(TextDirection.ltr))); + expect(new EdgeInsetsDirectional.fromSTEB(10.0, 20.0, 30.0, 40.0).resolve(TextDirection.ltr), + isNot(new EdgeInsetsDirectional.fromSTEB(10.0, 20.0, 30.0, 40.0).resolve(TextDirection.rtl))); + }); + + test('EdgeInsets equality', () { + expect(new EdgeInsetsDirectional.only(top: 5.0, bottom: 7.0), new EdgeInsetsDirectional.only(top: 5.0, bottom: 7.0)); + expect(new EdgeInsets.only(top: 5.0, bottom: 7.0), new EdgeInsetsDirectional.only(top: 5.0, bottom: 7.0)); + expect(new EdgeInsetsDirectional.only(top: 5.0, bottom: 7.0), new EdgeInsets.only(top: 5.0, bottom: 7.0)); + expect(new EdgeInsets.only(top: 5.0, bottom: 7.0), new EdgeInsets.only(top: 5.0, bottom: 7.0)); + expect(new EdgeInsetsDirectional.only(start: 5.0), new EdgeInsetsDirectional.only(start: 5.0)); + expect(new EdgeInsets.only(left: 5.0), isNot(new EdgeInsetsDirectional.only(start: 5.0))); + expect(new EdgeInsetsDirectional.only(start: 5.0), isNot(new EdgeInsets.only(left: 5.0))); + expect(new EdgeInsets.only(left: 5.0), new EdgeInsets.only(left: 5.0)); + expect(new EdgeInsetsDirectional.only(end: 5.0), new EdgeInsetsDirectional.only(end: 5.0)); + expect(new EdgeInsets.only(right: 5.0), isNot(new EdgeInsetsDirectional.only(end: 5.0))); + expect(new EdgeInsetsDirectional.only(end: 5.0), isNot(new EdgeInsets.only(right: 5.0))); + expect(new EdgeInsets.only(right: 5.0), new EdgeInsets.only(right: 5.0)); + expect(new EdgeInsetsDirectional.only(end: 5.0).add(new EdgeInsets.only(right: 5.0)), new EdgeInsetsDirectional.only(end: 5.0).add(new EdgeInsets.only(right: 5.0))); + expect(new EdgeInsetsDirectional.only(end: 5.0).add(new EdgeInsets.only(right: 5.0)), isNot(new EdgeInsetsDirectional.only(end: 5.0).add(new EdgeInsets.only(left: 5.0)))); + expect(new EdgeInsetsDirectional.only(top: 1.0).add(new EdgeInsets.only(top: 2.0)), new EdgeInsetsDirectional.only(top: 3.0).add(new EdgeInsets.only(top: 0.0))); + expect(new EdgeInsetsDirectional.only(top: 1.0).add(new EdgeInsets.only(top: 2.0)), new EdgeInsets.only(top: 3.0).add(new EdgeInsetsDirectional.only(top: 0.0))); + expect(new EdgeInsetsDirectional.only(top: 1.0).add(new EdgeInsets.only(top: 2.0)), new EdgeInsetsDirectional.only(top: 3.0)); + expect(new EdgeInsetsDirectional.only(top: 1.0).add(new EdgeInsets.only(top: 2.0)), new EdgeInsets.only(top: 3.0)); + }); + + test('EdgeInsetsGeometry.lerp(...)', () { + expect(EdgeInsetsGeometry.lerp(new EdgeInsetsDirectional.only(end: 10.0), null, 0.5), new EdgeInsetsDirectional.only(end: 5.0)); + expect(EdgeInsetsGeometry.lerp(new EdgeInsetsDirectional.only(start: 10.0), null, 0.5), new EdgeInsetsDirectional.only(start: 5.0)); + expect(EdgeInsetsGeometry.lerp(new EdgeInsetsDirectional.only(top: 10.0), null, 0.5), new EdgeInsetsDirectional.only(top: 5.0)); + expect(EdgeInsetsGeometry.lerp(new EdgeInsetsDirectional.only(bottom: 10.0), null, 0.5), new EdgeInsetsDirectional.only(bottom: 5.0)); + expect(EdgeInsetsGeometry.lerp(new EdgeInsetsDirectional.only(bottom: 10.0), EdgeInsetsDirectional.zero, 0.5), new EdgeInsetsDirectional.only(bottom: 5.0)); + expect(EdgeInsetsGeometry.lerp(new EdgeInsetsDirectional.only(bottom: 10.0), EdgeInsets.zero, 0.5), new EdgeInsetsDirectional.only(bottom: 5.0)); + expect(EdgeInsetsGeometry.lerp(new EdgeInsetsDirectional.only(start: 10.0), new EdgeInsets.only(left: 20.0), 0.5), new EdgeInsetsDirectional.only(start: 5.0).add(new EdgeInsets.only(left: 10.0))); + expect(EdgeInsetsGeometry.lerp(new EdgeInsetsDirectional.only(start: 0.0, bottom: 1.0), new EdgeInsetsDirectional.only(start: 1.0, bottom: 1.0).add(new EdgeInsets.only(right: 2.0, bottom: 0.0)), 0.5), new EdgeInsetsDirectional.only(start: 0.5).add(new EdgeInsets.only(right: 1.0, bottom: 1.0))); + expect(EdgeInsetsGeometry.lerp(new EdgeInsets.only(left: 0.0, bottom: 1.0), new EdgeInsetsDirectional.only(end: 1.0, bottom: 1.0).add(new EdgeInsets.only(right: 2.0, bottom: 0.0)), 0.5), new EdgeInsetsDirectional.only(start: 0.0, end: 0.5).add(new EdgeInsets.only(right: 1.0, bottom: 1.0))); + }); + + test('EdgeInsetsGeometry.lerp(normal, ...)', () { + final EdgeInsets a = const EdgeInsets.all(10.0); + final EdgeInsets b = const EdgeInsets.all(20.0); + expect(EdgeInsetsGeometry.lerp(a, b, 0.25), equals(a * 1.25)); + expect(EdgeInsetsGeometry.lerp(a, b, 0.25), equals(b * 0.625)); + expect(EdgeInsetsGeometry.lerp(a, b, 0.25), equals(a + const EdgeInsets.all(2.5))); + expect(EdgeInsetsGeometry.lerp(a, b, 0.25), equals(b - const EdgeInsets.all(7.5))); + + expect(EdgeInsetsGeometry.lerp(null, null, 0.25), isNull); + expect(EdgeInsetsGeometry.lerp(null, b, 0.25), equals(b * 0.25)); + expect(EdgeInsetsGeometry.lerp(a, null, 0.25), equals(a * 0.75)); + }); + + test('EdgeInsetsGeometry.lerp(directional, ...)', () { + final EdgeInsetsDirectional a = const EdgeInsetsDirectional.only(start: 10.0, end: 10.0); + final EdgeInsetsDirectional b = const EdgeInsetsDirectional.only(start: 20.0, end: 20.0); + expect(EdgeInsetsGeometry.lerp(a, b, 0.25), equals(a * 1.25)); + expect(EdgeInsetsGeometry.lerp(a, b, 0.25), equals(b * 0.625)); + expect(EdgeInsetsGeometry.lerp(a, b, 0.25), equals(a + const EdgeInsetsDirectional.only(start: 2.5, end: 2.5))); + expect(EdgeInsetsGeometry.lerp(a, b, 0.25), equals(b - const EdgeInsetsDirectional.only(start: 7.5, end: 7.5))); + + expect(EdgeInsetsGeometry.lerp(null, null, 0.25), isNull); + expect(EdgeInsetsGeometry.lerp(null, b, 0.25), equals(b * 0.25)); + expect(EdgeInsetsGeometry.lerp(a, null, 0.25), equals(a * 0.75)); + }); + + test('EdgeInsetsGeometry.lerp(mixed, ...)', () { + final EdgeInsetsGeometry a = const EdgeInsetsDirectional.only(start: 10.0, end: 10.0).add(const EdgeInsets.all(1.0)); + final EdgeInsetsGeometry b = const EdgeInsetsDirectional.only(start: 20.0, end: 20.0).add(const EdgeInsets.all(2.0)); + expect(EdgeInsetsGeometry.lerp(a, b, 0.25), equals(a * 1.25)); + expect(EdgeInsetsGeometry.lerp(a, b, 0.25), equals(b * 0.625)); + + expect(EdgeInsetsGeometry.lerp(null, null, 0.25), isNull); + expect(EdgeInsetsGeometry.lerp(null, b, 0.25), equals(b * 0.25)); + expect(EdgeInsetsGeometry.lerp(a, null, 0.25), equals(a * 0.75)); + }); + + test('EdgeInsets operators', () { + const EdgeInsets a = const EdgeInsets.fromLTRB(1.0, 2.0, 3.0, 5.0); + expect(a * 2.0, const EdgeInsets.fromLTRB(2.0, 4.0, 6.0, 10.0)); + expect(a / 2.0, const EdgeInsets.fromLTRB(0.5, 1.0, 1.5, 2.5)); + expect(a % 2.0, const EdgeInsets.fromLTRB(1.0, 0.0, 1.0, 1.0)); + expect(a ~/ 2.0, const EdgeInsets.fromLTRB(0.0, 1.0, 1.0, 2.0)); + expect(a + a, a * 2.0); + expect(a - a, EdgeInsets.zero); + expect(a.add(a), a * 2.0); + expect(a.subtract(a), EdgeInsets.zero); + }); + + test('EdgeInsetsDirectional operators', () { + const EdgeInsetsDirectional a = const EdgeInsetsDirectional.fromSTEB(1.0, 2.0, 3.0, 5.0); + expect(a * 2.0, const EdgeInsetsDirectional.fromSTEB(2.0, 4.0, 6.0, 10.0)); + expect(a / 2.0, const EdgeInsetsDirectional.fromSTEB(0.5, 1.0, 1.5, 2.5)); + expect(a % 2.0, const EdgeInsetsDirectional.fromSTEB(1.0, 0.0, 1.0, 1.0)); + expect(a ~/ 2.0, const EdgeInsetsDirectional.fromSTEB(0.0, 1.0, 1.0, 2.0)); + expect(a + a, a * 2.0); + expect(a - a, EdgeInsetsDirectional.zero); + expect(a.add(a), a * 2.0); + expect(a.subtract(a), EdgeInsetsDirectional.zero); + }); + + test('EdgeInsetsGeometry operators', () { + final EdgeInsetsGeometry a = const EdgeInsetsDirectional.fromSTEB(1.0, 2.0, 3.0, 5.0).add(EdgeInsets.zero); + expect(a, isNot(new isInstanceOf())); + expect(a * 2.0, const EdgeInsetsDirectional.fromSTEB(2.0, 4.0, 6.0, 10.0)); + expect(a / 2.0, const EdgeInsetsDirectional.fromSTEB(0.5, 1.0, 1.5, 2.5)); + expect(a % 2.0, const EdgeInsetsDirectional.fromSTEB(1.0, 0.0, 1.0, 1.0)); + expect(a ~/ 2.0, const EdgeInsetsDirectional.fromSTEB(0.0, 1.0, 1.0, 2.0)); + expect(a.add(a), a * 2.0); + expect(a.subtract(a), EdgeInsetsDirectional.zero); + expect(a.subtract(a), EdgeInsets.zero); + }); + + test('EdgeInsetsGeometry toString', () { + expect(new EdgeInsets.only().toString(), 'EdgeInsets.zero'); + expect(new EdgeInsets.only(top: 1.01, left: 1.01, right: 1.01, bottom: 1.01).toString(), 'EdgeInsets.all(1.0)'); + expect(new EdgeInsetsDirectional.only().toString(), 'EdgeInsets.zero'); + expect(new EdgeInsetsDirectional.only(start: 1.01, end: 1.01, top: 1.01, bottom: 1.01).toString(), 'EdgeInsetsDirectional(1.0, 1.0, 1.0, 1.0)'); + expect((new EdgeInsetsDirectional.only(start: 4.0).add(new EdgeInsets.only(top: 3.0))).toString(), 'EdgeInsetsDirectional(4.0, 3.0, 0.0, 0.0)'); + expect((new EdgeInsetsDirectional.only(top: 4.0).add(new EdgeInsets.only(right: 3.0))).toString(), 'EdgeInsets(0.0, 4.0, 3.0, 0.0)'); + expect((new EdgeInsetsDirectional.only(start: 4.0).add(new EdgeInsets.only(left: 3.0))).toString(), 'EdgeInsets(3.0, 0.0, 0.0, 0.0) + EdgeInsetsDirectional(4.0, 0.0, 0.0, 0.0)'); + }); } diff --git a/packages/flutter/test/painting/fractional_offset_test.dart b/packages/flutter/test/painting/fractional_offset_test.dart index c89d9a11c5..bfb8e81543 100644 --- a/packages/flutter/test/painting/fractional_offset_test.dart +++ b/packages/flutter/test/painting/fractional_offset_test.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:ui' show lerpDouble; + import 'package:flutter/painting.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -23,8 +25,8 @@ void main() { expect(FractionalOffset.lerp(a, b, 0.25), equals(const FractionalOffset(0.125, 0.0))); expect(FractionalOffset.lerp(null, null, 0.25), isNull); - expect(FractionalOffset.lerp(null, b, 0.25), equals(b * 0.25)); - expect(FractionalOffset.lerp(a, null, 0.25), equals(a * 0.75)); + expect(FractionalOffset.lerp(null, b, 0.25), equals(new FractionalOffset(0.5, 0.5 - 0.125))); + expect(FractionalOffset.lerp(a, null, 0.25), equals(new FractionalOffset(0.125, 0.125))); }); test('FractionalOffset.fromOffsetAndSize()', () { @@ -36,4 +38,107 @@ void main() { final FractionalOffset a = new FractionalOffset.fromOffsetAndRect(const Offset(150.0, 120.0), new Rect.fromLTWH(50.0, 20.0, 200.0, 400.0)); expect(a, const FractionalOffset(0.5, 0.25)); }); + + test('FractionalOffsetGeometry.resolve()', () { + expect(new FractionalOffsetDirectional(0.25, 0.3).resolve(TextDirection.ltr), const FractionalOffset(0.25, 0.3)); + expect(new FractionalOffsetDirectional(0.25, 0.3).resolve(TextDirection.rtl), const FractionalOffset(0.75, 0.3)); + expect(new FractionalOffsetDirectional(-0.25, 0.3).resolve(TextDirection.ltr), const FractionalOffset(-0.25, 0.3)); + expect(new FractionalOffsetDirectional(-0.25, 0.3).resolve(TextDirection.rtl), const FractionalOffset(1.25, 0.3)); + expect(new FractionalOffsetDirectional(1.25, 0.3).resolve(TextDirection.ltr), const FractionalOffset(1.25, 0.3)); + expect(new FractionalOffsetDirectional(1.25, 0.3).resolve(TextDirection.rtl), const FractionalOffset(-0.25, 0.3)); + expect(new FractionalOffsetDirectional(0.5, -0.3).resolve(TextDirection.ltr), const FractionalOffset(0.5, -0.3)); + expect(new FractionalOffsetDirectional(0.5, -0.3).resolve(TextDirection.rtl), const FractionalOffset(0.5, -0.3)); + expect(new FractionalOffsetDirectional(0.0, 0.0).resolve(TextDirection.ltr), const FractionalOffset(0.0, 0.0)); + expect(new FractionalOffsetDirectional(0.0, 0.0).resolve(TextDirection.rtl), const FractionalOffset(1.0, 0.0)); + expect(new FractionalOffsetDirectional(1.0, 1.0).resolve(TextDirection.ltr), const FractionalOffset(1.0, 1.0)); + expect(new FractionalOffsetDirectional(1.0, 1.0).resolve(TextDirection.rtl), const FractionalOffset(0.0, 1.0)); + expect(new FractionalOffsetDirectional(1.0, 2.0), new FractionalOffsetDirectional(1.0, 2.0)); + expect(new FractionalOffsetDirectional(1.0, 2.0).hashCode, isNot(new FractionalOffsetDirectional(2.0, 1.0))); + expect(new FractionalOffsetDirectional(0.0, 0.0).resolve(TextDirection.ltr), + new FractionalOffsetDirectional(1.0, 0.0).resolve(TextDirection.rtl)); + expect(new FractionalOffsetDirectional(0.0, 0.0).resolve(TextDirection.ltr), + isNot(new FractionalOffsetDirectional(1.0, 0.0).resolve(TextDirection.ltr))); + expect(new FractionalOffsetDirectional(1.0, 0.0).resolve(TextDirection.ltr), + isNot(new FractionalOffsetDirectional(1.0, 0.0).resolve(TextDirection.rtl))); + }); + + test('FractionalOffsetGeometry.lerp', () { + final FractionalOffsetGeometry directional1 = const FractionalOffsetDirectional(0.125, 0.625); + final FractionalOffsetGeometry normal1 = const FractionalOffset(0.25, 0.875); + final FractionalOffsetGeometry mixed1 = const FractionalOffset(0.0625, 0.5625).add(const FractionalOffsetDirectional(0.1875, 0.6875)); + final FractionalOffsetGeometry directional2 = const FractionalOffsetDirectional(2.0, 3.0); + final FractionalOffsetGeometry normal2 = const FractionalOffset(2.0, 3.0); + final FractionalOffsetGeometry mixed2 = const FractionalOffset(2.0, 3.0).add(const FractionalOffsetDirectional(5.0, 3.0)); + + expect(FractionalOffsetGeometry.lerp(directional1, directional2, 0.5), const FractionalOffsetDirectional(0.125 + (2.0 - 0.125) / 2.0, 0.625 + (3.0 - 0.625) / 2.0)); + expect(FractionalOffsetGeometry.lerp(directional2, directional2, 0.5), directional2); + expect(FractionalOffsetGeometry.lerp(directional1, normal2, 0.5).resolve(TextDirection.ltr), const FractionalOffset(1.0 + 1.0 / 16.0, 0.625 + (3.0 - 0.625) / 2.0)); + expect(FractionalOffsetGeometry.lerp(directional1, normal2, 0.5).resolve(TextDirection.rtl), const FractionalOffset(1.0 + 15.0 / 16.0, 0.625 + (3.0 - 0.625) / 2.0)); + expect(FractionalOffsetGeometry.lerp(directional1, mixed1, 0.5).resolve(TextDirection.ltr), new FractionalOffset(1.0 / 32.0 + 2.5 / 16.0, lerpDouble(0.625, 0.5625 + 0.6875, 0.5))); + expect(FractionalOffsetGeometry.lerp(directional1, mixed1, 0.5).resolve(TextDirection.rtl), new FractionalOffset(1.0 / 32.0 + 1.0 - 2.5 / 16.0, lerpDouble(0.625, 0.5625 + 0.6875, 0.5))); + expect(FractionalOffsetGeometry.lerp(mixed1, mixed2, 0.5).resolve(TextDirection.ltr), new FractionalOffset(3.0 + 5.0 / 8.0, lerpDouble(0.5625 + 0.6875, 6.0, 0.5))); + expect(FractionalOffsetGeometry.lerp(mixed1, mixed2, 0.5).resolve(TextDirection.rtl), new FractionalOffset(2.0 - 41.0 / 16.0, lerpDouble(0.5625 + 0.6875, 6.0, 0.5))); + expect(FractionalOffsetGeometry.lerp(normal1, normal2, 0.5), const FractionalOffset(0.25 + (2.0 - 0.25) / 2.0, 0.875 + (3.0 - 0.875) / 2.0)); + expect(FractionalOffsetGeometry.lerp(normal1, mixed1, 0.5).resolve(TextDirection.ltr), new FractionalOffset(lerpDouble(0.25, 0.0625, 0.5) + lerpDouble(0.0, 0.1875, 0.5), lerpDouble(0.875, 0.5625 + 0.6875, 0.5))); + expect(FractionalOffsetGeometry.lerp(normal1, mixed1, 0.5).resolve(TextDirection.rtl), new FractionalOffset(lerpDouble(0.25, 0.0625, 0.5) + 1.0 - lerpDouble(0.0, 0.1875, 0.5), lerpDouble(0.875, 0.5625 + 0.6875, 0.5))); + expect(FractionalOffsetGeometry.lerp(null, mixed1, 0.5).resolve(TextDirection.ltr), FractionalOffsetGeometry.lerp(FractionalOffset.center, mixed1, 0.5).resolve(TextDirection.ltr)); + expect(FractionalOffsetGeometry.lerp(mixed2, null, 0.25).resolve(TextDirection.ltr), FractionalOffsetGeometry.lerp(FractionalOffset.center, mixed2, 0.75).resolve(TextDirection.ltr)); + expect(FractionalOffsetGeometry.lerp(directional1, null, 1.0), FractionalOffsetDirectional.center); + expect(FractionalOffsetGeometry.lerp(null, null, 0.5), isNull); + }); + + test('FractionalOffsetGeometry.lerp more', () { + final FractionalOffsetGeometry mixed1 = const FractionalOffset(10.0, 20.0).add(const FractionalOffsetDirectional(30.0, 50.0)); + final FractionalOffsetGeometry mixed2 = const FractionalOffset(70.0, 110.0).add(const FractionalOffsetDirectional(130.0, 170.0)); + final FractionalOffsetGeometry mixed3 = const FractionalOffset(25.0, 42.5).add(const FractionalOffsetDirectional(55.0, 80.0)); + + expect(FractionalOffsetGeometry.lerp(mixed1, mixed2, 0.0), mixed1); + expect(FractionalOffsetGeometry.lerp(mixed1, mixed2, 1.0), mixed2); + expect(FractionalOffsetGeometry.lerp(mixed1, mixed2, 0.25), mixed3); + }); + + test('FractionalOffsetGeometry add/subtract', () { + final FractionalOffsetGeometry directional = const FractionalOffsetDirectional(1.0, 2.0); + final FractionalOffsetGeometry normal = const FractionalOffset(3.0, 5.0); + expect(directional.add(normal).resolve(TextDirection.ltr), const FractionalOffset(4.0, 7.0)); + expect(directional.add(normal).resolve(TextDirection.rtl), const FractionalOffset(3.0, 7.0)); + expect(directional.subtract(normal).resolve(TextDirection.ltr), const FractionalOffset(-2.0, -3.0)); + expect(directional.subtract(normal).resolve(TextDirection.rtl), const FractionalOffset(-3.0, -3.0)); + expect(normal.add(normal), normal * 2.0); + expect(normal.subtract(normal), FractionalOffset.topLeft); + expect(directional.add(directional), directional * 2.0); + expect(directional.subtract(directional), FractionalOffsetDirectional.topStart); + }); + + test('FractionalOffsetGeometry operators', () { + expect(new FractionalOffsetDirectional(1.0, 2.0) * 2.0, new FractionalOffsetDirectional(2.0, 4.0)); + expect(new FractionalOffsetDirectional(1.0, 2.0) / 2.0, new FractionalOffsetDirectional(0.5, 1.0)); + expect(new FractionalOffsetDirectional(1.0, 2.0) % 2.0, new FractionalOffsetDirectional(1.0, 0.0)); + expect(new FractionalOffsetDirectional(1.0, 2.0) ~/ 2.0, new FractionalOffsetDirectional(0.0, 1.0)); + expect(FractionalOffset.topLeft.add(new FractionalOffsetDirectional(1.0, 2.0) * 2.0), new FractionalOffsetDirectional(2.0, 4.0)); + expect(FractionalOffset.topLeft.add(new FractionalOffsetDirectional(1.0, 2.0) / 2.0), new FractionalOffsetDirectional(0.5, 1.0)); + expect(FractionalOffset.topLeft.add(new FractionalOffsetDirectional(1.0, 2.0) % 2.0), new FractionalOffsetDirectional(1.0, 0.0)); + expect(FractionalOffset.topLeft.add(new FractionalOffsetDirectional(1.0, 2.0) ~/ 2.0), new FractionalOffsetDirectional(0.0, 1.0)); + expect(new FractionalOffset(1.0, 2.0) * 2.0, new FractionalOffset(2.0, 4.0)); + expect(new FractionalOffset(1.0, 2.0) / 2.0, new FractionalOffset(0.5, 1.0)); + expect(new FractionalOffset(1.0, 2.0) % 2.0, new FractionalOffset(1.0, 0.0)); + expect(new FractionalOffset(1.0, 2.0) ~/ 2.0, new FractionalOffset(0.0, 1.0)); + }); + + test('FractionalOffsetGeometry operators', () { + expect(new FractionalOffset(1.0, 2.0) + new FractionalOffset(3.0, 5.0), new FractionalOffset(4.0, 7.0)); + expect(new FractionalOffset(1.0, 2.0) - new FractionalOffset(3.0, 5.0), new FractionalOffset(-2.0, -3.0)); + expect(new FractionalOffsetDirectional(1.0, 2.0) + new FractionalOffsetDirectional(3.0, 5.0), new FractionalOffsetDirectional(4.0, 7.0)); + expect(new FractionalOffsetDirectional(1.0, 2.0) - new FractionalOffsetDirectional(3.0, 5.0), new FractionalOffsetDirectional(-2.0, -3.0)); + }); + + test('FractionalOffsetGeometry toString', () { + expect(new FractionalOffset(1.0001, 2.0001).toString(), 'FractionalOffset(1.0, 2.0)'); + expect(new FractionalOffset(0.0, 0.0).toString(), 'FractionalOffset.topLeft'); + expect(new FractionalOffset(0.0, 1.0).add(new FractionalOffsetDirectional(1.0, 0.0)).toString(), 'FractionalOffsetDirectional.bottomEnd'); + expect(new FractionalOffset(0.0001, 0.0001).toString(), 'FractionalOffset(0.0, 0.0)'); + expect(new FractionalOffset(0.0, 0.0).toString(), 'FractionalOffset.topLeft'); + expect(new FractionalOffsetDirectional(0.0, 0.0).toString(), 'FractionalOffsetDirectional.topStart'); + expect(new FractionalOffset(1.0, 1.0).add(new FractionalOffsetDirectional(1.0, 1.0)).toString(), 'FractionalOffset(1.0, 2.0) + FractionalOffsetDirectional(1.0, 0.0)'); + }); } diff --git a/packages/flutter/test/rendering/flex_test.dart b/packages/flutter/test/rendering/flex_test.dart index 75836310b2..87ab3ff766 100644 --- a/packages/flutter/test/rendering/flex_test.dart +++ b/packages/flutter/test/rendering/flex_test.dart @@ -11,13 +11,13 @@ import 'rendering_tester.dart'; void main() { test('Overconstrained flex', () { final RenderDecoratedBox box = new RenderDecoratedBox(decoration: const BoxDecoration()); - final RenderFlex flex = new RenderFlex(children: [box]); + final RenderFlex flex = new RenderFlex(textDirection: TextDirection.ltr, children: [box]); layout(flex, constraints: const BoxConstraints( minWidth: 200.0, maxWidth: 200.0, minHeight: 200.0, maxHeight: 200.0) ); - expect(flex.size.width, equals(200.0), reason: "flex width"); - expect(flex.size.height, equals(200.0), reason: "flex height"); + expect(flex.size.width, equals(200.0), reason: 'flex width'); + expect(flex.size.height, equals(200.0), reason: 'flex height'); }); test('Vertical Overflow', () { @@ -26,6 +26,7 @@ void main() { ); final RenderFlex flex = new RenderFlex( direction: Axis.vertical, + verticalDirection: VerticalDirection.down, children: [ new RenderConstrainedBox(additionalConstraints: const BoxConstraints.tightFor(height: 200.0)), flexible, @@ -48,6 +49,7 @@ void main() { ); final RenderFlex flex = new RenderFlex( direction: Axis.horizontal, + textDirection: TextDirection.ltr, children: [ new RenderConstrainedBox(additionalConstraints: const BoxConstraints.tightFor(width: 200.0)), flexible, @@ -67,6 +69,7 @@ void main() { test('Vertical Flipped Constraints', () { final RenderFlex flex = new RenderFlex( direction: Axis.vertical, + verticalDirection: VerticalDirection.down, children: [ new RenderAspectRatio(aspectRatio: 1.0), ] @@ -95,7 +98,7 @@ void main() { ' mainAxisAlignment: start\n' ' mainAxisSize: max\n' ' crossAxisAlignment: center\n' - ' textBaseline: null\n' + ' verticalDirection: down\n' ), ); }); @@ -103,7 +106,7 @@ void main() { test('Parent data', () { final RenderDecoratedBox box1 = new RenderDecoratedBox(decoration: const BoxDecoration()); final RenderDecoratedBox box2 = new RenderDecoratedBox(decoration: const BoxDecoration()); - final RenderFlex flex = new RenderFlex(children: [box1, box2]); + final RenderFlex flex = new RenderFlex(textDirection: TextDirection.ltr, children: [box1, box2]); layout(flex, constraints: const BoxConstraints( minWidth: 0.0, maxWidth: 100.0, minHeight: 0.0, maxHeight: 100.0) ); @@ -125,7 +128,7 @@ void main() { test('Stretch', () { final RenderDecoratedBox box1 = new RenderDecoratedBox(decoration: const BoxDecoration()); final RenderDecoratedBox box2 = new RenderDecoratedBox(decoration: const BoxDecoration()); - final RenderFlex flex = new RenderFlex(); + final RenderFlex flex = new RenderFlex(textDirection: TextDirection.ltr); flex.setupParentData(box2); final FlexParentData box2ParentData = box2.parentData; box2ParentData.flex = 2; @@ -157,7 +160,7 @@ void main() { final RenderConstrainedBox box1 = new RenderConstrainedBox(additionalConstraints: const BoxConstraints.tightFor(width: 100.0, height: 100.0)); final RenderConstrainedBox box2 = new RenderConstrainedBox(additionalConstraints: const BoxConstraints.tightFor(width: 100.0, height: 100.0)); final RenderConstrainedBox box3 = new RenderConstrainedBox(additionalConstraints: const BoxConstraints.tightFor(width: 100.0, height: 100.0)); - final RenderFlex flex = new RenderFlex(mainAxisAlignment: MainAxisAlignment.spaceEvenly); + final RenderFlex flex = new RenderFlex(textDirection: TextDirection.ltr, mainAxisAlignment: MainAxisAlignment.spaceEvenly); flex.addAll([box1, box2, box3]); layout(flex, constraints: const BoxConstraints( minWidth: 0.0, maxWidth: 500.0, minHeight: 0.0, maxHeight: 400.0) @@ -187,7 +190,7 @@ void main() { final RenderConstrainedBox box1 = new RenderConstrainedBox(additionalConstraints: const BoxConstraints.tightFor(width: 100.0, height: 100.0)); final RenderConstrainedBox box2 = new RenderConstrainedBox(additionalConstraints: const BoxConstraints.tightFor(width: 100.0, height: 100.0)); final RenderConstrainedBox box3 = new RenderConstrainedBox(additionalConstraints: const BoxConstraints.tightFor(width: 100.0, height: 100.0)); - final RenderFlex flex = new RenderFlex(mainAxisAlignment: MainAxisAlignment.spaceBetween); + final RenderFlex flex = new RenderFlex(textDirection: TextDirection.ltr, mainAxisAlignment: MainAxisAlignment.spaceBetween); flex.addAll([box1, box2, box3]); layout(flex, constraints: const BoxConstraints( minWidth: 0.0, maxWidth: 500.0, minHeight: 0.0, maxHeight: 400.0) @@ -236,8 +239,9 @@ void main() { final RenderConstrainedBox box2 = new RenderConstrainedBox(additionalConstraints: const BoxConstraints.tightFor(width: 100.0, height: 100.0)); final RenderConstrainedBox box3 = new RenderConstrainedBox(additionalConstraints: const BoxConstraints.tightFor(width: 100.0, height: 100.0)); final RenderFlex flex = new RenderFlex( + textDirection: TextDirection.ltr, mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.spaceBetween + mainAxisAlignment: MainAxisAlignment.spaceBetween, ); flex.addAll([box1, box2, box3]); layout(flex, constraints: const BoxConstraints( @@ -293,6 +297,7 @@ void main() { final RenderConstrainedBox box2 = new RenderConstrainedBox(additionalConstraints: square); final RenderConstrainedBox box3 = new RenderConstrainedBox(additionalConstraints: square); final RenderFlex flex = new RenderFlex( + textDirection: TextDirection.ltr, mainAxisSize: MainAxisSize.min, ); final RenderConstrainedOverflowBox parent = new RenderConstrainedOverflowBox( @@ -337,6 +342,7 @@ void main() { final RenderConstrainedBox box2 = new RenderConstrainedBox(additionalConstraints: square); final RenderConstrainedBox box3 = new RenderConstrainedBox(additionalConstraints: square); final RenderFlex flex = new RenderFlex( + textDirection: TextDirection.ltr, mainAxisSize: MainAxisSize.min, ); final RenderConstrainedOverflowBox parent = new RenderConstrainedOverflowBox( @@ -365,6 +371,7 @@ void main() { final RenderConstrainedBox box2 = new RenderConstrainedBox(additionalConstraints: square); final RenderConstrainedBox box3 = new RenderConstrainedBox(additionalConstraints: square); final RenderFlex flex = new RenderFlex( + textDirection: TextDirection.ltr, mainAxisSize: MainAxisSize.max, ); final RenderConstrainedOverflowBox parent = new RenderConstrainedOverflowBox( @@ -383,4 +390,154 @@ void main() { expect(exceptions, isNotEmpty); expect(exceptions.first, new isInstanceOf()); }); + + test('Flex RTL', () { + final BoxConstraints square = const BoxConstraints.tightFor(width: 100.0, height: 100.0); + final RenderConstrainedBox box1 = new RenderConstrainedBox(additionalConstraints: square); + final RenderConstrainedBox box2 = new RenderConstrainedBox(additionalConstraints: square); + final RenderConstrainedBox box3 = new RenderConstrainedBox(additionalConstraints: square); + final RenderFlex flex = new RenderFlex(textDirection: TextDirection.ltr, children: [box1, box2, box3]); + layout(flex); + expect(box1.localToGlobal(const Offset(0.0, 0.0)), const Offset(0.0, 250.0)); + expect(box2.localToGlobal(const Offset(0.0, 0.0)), const Offset(100.0, 250.0)); + expect(box3.localToGlobal(const Offset(0.0, 0.0)), const Offset(200.0, 250.0)); + expect(box1.size, const Size(100.0, 100.0)); + expect(box2.size, const Size(100.0, 100.0)); + expect(box3.size, const Size(100.0, 100.0)); + + flex.mainAxisAlignment = MainAxisAlignment.end; + pumpFrame(); + expect(box1.localToGlobal(const Offset(0.0, 0.0)), const Offset(500.0, 250.0)); + expect(box2.localToGlobal(const Offset(0.0, 0.0)), const Offset(600.0, 250.0)); + expect(box3.localToGlobal(const Offset(0.0, 0.0)), const Offset(700.0, 250.0)); + expect(box1.size, const Size(100.0, 100.0)); + expect(box2.size, const Size(100.0, 100.0)); + expect(box3.size, const Size(100.0, 100.0)); + + flex.textDirection = TextDirection.rtl; + pumpFrame(); + expect(box1.localToGlobal(const Offset(0.0, 0.0)), const Offset(200.0, 250.0)); + expect(box2.localToGlobal(const Offset(0.0, 0.0)), const Offset(100.0, 250.0)); + expect(box3.localToGlobal(const Offset(0.0, 0.0)), const Offset(0.0, 250.0)); + expect(box1.size, const Size(100.0, 100.0)); + expect(box2.size, const Size(100.0, 100.0)); + expect(box3.size, const Size(100.0, 100.0)); + + flex.mainAxisAlignment = MainAxisAlignment.start; + pumpFrame(); + expect(box1.localToGlobal(const Offset(0.0, 0.0)), const Offset(700.0, 250.0)); + expect(box2.localToGlobal(const Offset(0.0, 0.0)), const Offset(600.0, 250.0)); + expect(box3.localToGlobal(const Offset(0.0, 0.0)), const Offset(500.0, 250.0)); + expect(box1.size, const Size(100.0, 100.0)); + expect(box2.size, const Size(100.0, 100.0)); + expect(box3.size, const Size(100.0, 100.0)); + + flex.crossAxisAlignment = CrossAxisAlignment.start; // vertical direction is down + pumpFrame(); + expect(box1.localToGlobal(const Offset(0.0, 0.0)), const Offset(700.0, 0.0)); + expect(box2.localToGlobal(const Offset(0.0, 0.0)), const Offset(600.0, 0.0)); + expect(box3.localToGlobal(const Offset(0.0, 0.0)), const Offset(500.0, 0.0)); + expect(box1.size, const Size(100.0, 100.0)); + expect(box2.size, const Size(100.0, 100.0)); + expect(box3.size, const Size(100.0, 100.0)); + + flex.crossAxisAlignment = CrossAxisAlignment.end; + pumpFrame(); + expect(box1.localToGlobal(const Offset(0.0, 0.0)), const Offset(700.0, 500.0)); + expect(box2.localToGlobal(const Offset(0.0, 0.0)), const Offset(600.0, 500.0)); + expect(box3.localToGlobal(const Offset(0.0, 0.0)), const Offset(500.0, 500.0)); + expect(box1.size, const Size(100.0, 100.0)); + expect(box2.size, const Size(100.0, 100.0)); + expect(box3.size, const Size(100.0, 100.0)); + + flex.verticalDirection = VerticalDirection.up; + pumpFrame(); + expect(box1.localToGlobal(const Offset(0.0, 0.0)), const Offset(700.0, 0.0)); + expect(box2.localToGlobal(const Offset(0.0, 0.0)), const Offset(600.0, 0.0)); + expect(box3.localToGlobal(const Offset(0.0, 0.0)), const Offset(500.0, 0.0)); + expect(box1.size, const Size(100.0, 100.0)); + expect(box2.size, const Size(100.0, 100.0)); + expect(box3.size, const Size(100.0, 100.0)); + + flex.crossAxisAlignment = CrossAxisAlignment.start; + pumpFrame(); + expect(box1.localToGlobal(const Offset(0.0, 0.0)), const Offset(700.0, 500.0)); + expect(box2.localToGlobal(const Offset(0.0, 0.0)), const Offset(600.0, 500.0)); + expect(box3.localToGlobal(const Offset(0.0, 0.0)), const Offset(500.0, 500.0)); + expect(box1.size, const Size(100.0, 100.0)); + expect(box2.size, const Size(100.0, 100.0)); + expect(box3.size, const Size(100.0, 100.0)); + + flex.direction = Axis.vertical; // and main=start, cross=start, up, rtl + pumpFrame(); + expect(box1.localToGlobal(const Offset(0.0, 0.0)), const Offset(700.0, 500.0)); + expect(box2.localToGlobal(const Offset(0.0, 0.0)), const Offset(700.0, 400.0)); + expect(box3.localToGlobal(const Offset(0.0, 0.0)), const Offset(700.0, 300.0)); + expect(box1.size, const Size(100.0, 100.0)); + expect(box2.size, const Size(100.0, 100.0)); + expect(box3.size, const Size(100.0, 100.0)); + + flex.crossAxisAlignment = CrossAxisAlignment.end; + pumpFrame(); + expect(box1.localToGlobal(const Offset(0.0, 0.0)), const Offset(0.0, 500.0)); + expect(box2.localToGlobal(const Offset(0.0, 0.0)), const Offset(0.0, 400.0)); + expect(box3.localToGlobal(const Offset(0.0, 0.0)), const Offset(0.0, 300.0)); + expect(box1.size, const Size(100.0, 100.0)); + expect(box2.size, const Size(100.0, 100.0)); + expect(box3.size, const Size(100.0, 100.0)); + + flex.crossAxisAlignment = CrossAxisAlignment.stretch; + pumpFrame(); + expect(box1.localToGlobal(const Offset(0.0, 0.0)), const Offset(0.0, 500.0)); + expect(box2.localToGlobal(const Offset(0.0, 0.0)), const Offset(0.0, 400.0)); + expect(box3.localToGlobal(const Offset(0.0, 0.0)), const Offset(0.0, 300.0)); + expect(box1.size, const Size(800.0, 100.0)); + expect(box2.size, const Size(800.0, 100.0)); + expect(box3.size, const Size(800.0, 100.0)); + + flex.textDirection = TextDirection.ltr; + pumpFrame(); + expect(box1.localToGlobal(const Offset(0.0, 0.0)), const Offset(0.0, 500.0)); + expect(box2.localToGlobal(const Offset(0.0, 0.0)), const Offset(0.0, 400.0)); + expect(box3.localToGlobal(const Offset(0.0, 0.0)), const Offset(0.0, 300.0)); + expect(box1.size, const Size(800.0, 100.0)); + expect(box2.size, const Size(800.0, 100.0)); + expect(box3.size, const Size(800.0, 100.0)); + + flex.crossAxisAlignment = CrossAxisAlignment.start; + pumpFrame(); + expect(box1.localToGlobal(const Offset(0.0, 0.0)), const Offset(0.0, 500.0)); + expect(box2.localToGlobal(const Offset(0.0, 0.0)), const Offset(0.0, 400.0)); + expect(box3.localToGlobal(const Offset(0.0, 0.0)), const Offset(0.0, 300.0)); + expect(box1.size, const Size(100.0, 100.0)); + expect(box2.size, const Size(100.0, 100.0)); + expect(box3.size, const Size(100.0, 100.0)); + + flex.crossAxisAlignment = CrossAxisAlignment.end; + pumpFrame(); + expect(box1.localToGlobal(const Offset(0.0, 0.0)), const Offset(700.0, 500.0)); + expect(box2.localToGlobal(const Offset(0.0, 0.0)), const Offset(700.0, 400.0)); + expect(box3.localToGlobal(const Offset(0.0, 0.0)), const Offset(700.0, 300.0)); + expect(box1.size, const Size(100.0, 100.0)); + expect(box2.size, const Size(100.0, 100.0)); + expect(box3.size, const Size(100.0, 100.0)); + + flex.verticalDirection = VerticalDirection.down; + pumpFrame(); + expect(box1.localToGlobal(const Offset(0.0, 0.0)), const Offset(700.0, 0.0)); + expect(box2.localToGlobal(const Offset(0.0, 0.0)), const Offset(700.0, 100.0)); + expect(box3.localToGlobal(const Offset(0.0, 0.0)), const Offset(700.0, 200.0)); + expect(box1.size, const Size(100.0, 100.0)); + expect(box2.size, const Size(100.0, 100.0)); + expect(box3.size, const Size(100.0, 100.0)); + + flex.mainAxisAlignment = MainAxisAlignment.end; + pumpFrame(); + expect(box1.localToGlobal(const Offset(0.0, 0.0)), const Offset(700.0, 300.0)); + expect(box2.localToGlobal(const Offset(0.0, 0.0)), const Offset(700.0, 400.0)); + expect(box3.localToGlobal(const Offset(0.0, 0.0)), const Offset(700.0, 500.0)); + expect(box1.size, const Size(100.0, 100.0)); + expect(box2.size, const Size(100.0, 100.0)); + expect(box3.size, const Size(100.0, 100.0)); + }); } diff --git a/packages/flutter/test/rendering/limited_box_test.dart b/packages/flutter/test/rendering/limited_box_test.dart index 69a65c2243..266f2e94dd 100644 --- a/packages/flutter/test/rendering/limited_box_test.dart +++ b/packages/flutter/test/rendering/limited_box_test.dart @@ -36,7 +36,7 @@ void main() { ' │ parentData: \n' ' │ constraints: BoxConstraints(w=800.0, h=600.0)\n' ' │ size: Size(800.0, 600.0)\n' - ' │ alignment: FractionalOffset(0.5, 0.5)\n' + ' │ alignment: FractionalOffset.center\n' ' │ minWidth: 0.0\n' ' │ maxWidth: Infinity\n' ' │ minHeight: 0.0\n' @@ -122,7 +122,7 @@ void main() { ' │ parentData: \n' ' │ constraints: BoxConstraints(w=800.0, h=600.0)\n' ' │ size: Size(800.0, 600.0)\n' - ' │ alignment: FractionalOffset(0.5, 0.5)\n' + ' │ alignment: FractionalOffset.center\n' ' │ minWidth: 10.0\n' ' │ maxWidth: 500.0\n' ' │ minHeight: 0.0\n' @@ -158,7 +158,7 @@ void main() { ' │ parentData: \n' ' │ constraints: BoxConstraints(w=800.0, h=600.0)\n' ' │ size: Size(800.0, 600.0)\n' - ' │ alignment: FractionalOffset(0.5, 0.5)\n' + ' │ alignment: FractionalOffset.center\n' ' │ minWidth: 10.0\n' ' │ maxWidth: use parent maxWidth constraint\n' ' │ minHeight: use parent minHeight constraint\n' diff --git a/packages/flutter/test/rendering/mutations_test.dart b/packages/flutter/test/rendering/mutations_test.dart index 431e3792cb..4a1c09ad09 100644 --- a/packages/flutter/test/rendering/mutations_test.dart +++ b/packages/flutter/test/rendering/mutations_test.dart @@ -36,7 +36,7 @@ void main() { RenderBox child1, child2; bool movedChild1 = false; bool movedChild2 = false; - final RenderFlex block = new RenderFlex(); + final RenderFlex block = new RenderFlex(textDirection: TextDirection.ltr); block.add(child1 = new RenderLayoutTestBox(() { movedChild1 = true; })); block.add(child2 = new RenderLayoutTestBox(() { movedChild2 = true; })); diff --git a/packages/flutter/test/widgets/automatic_keep_alive_test.dart b/packages/flutter/test/widgets/automatic_keep_alive_test.dart index 0f15b934b9..8b73f9fae5 100644 --- a/packages/flutter/test/widgets/automatic_keep_alive_test.dart +++ b/packages/flutter/test/widgets/automatic_keep_alive_test.dart @@ -196,32 +196,35 @@ void main() { group('Implied automatic keep-alive', () { tests(impliedMode: true); }); testWidgets('AutomaticKeepAlive double', (WidgetTester tester) async { - await tester.pumpWidget(new ListView( - addAutomaticKeepAlives: false, - addRepaintBoundaries: false, - children: [ - new AutomaticKeepAlive( - child: new Container( - height: 400.0, - child: new Row(children: [ - new Leaf(key: const GlobalObjectKey<_LeafState>(0), child: const Placeholder()), - new Leaf(key: const GlobalObjectKey<_LeafState>(1), child: const Placeholder()), - ]), + await tester.pumpWidget(new Directionality( + textDirection: TextDirection.ltr, + child: new ListView( + addAutomaticKeepAlives: false, + addRepaintBoundaries: false, + children: [ + new AutomaticKeepAlive( + child: new Container( + height: 400.0, + child: new Row(children: [ + new Leaf(key: const GlobalObjectKey<_LeafState>(0), child: const Placeholder()), + new Leaf(key: const GlobalObjectKey<_LeafState>(1), child: const Placeholder()), + ]), + ), ), - ), - new AutomaticKeepAlive( - child: new Container( - key: const GlobalObjectKey<_LeafState>(2), - height: 400.0, + new AutomaticKeepAlive( + child: new Container( + key: const GlobalObjectKey<_LeafState>(2), + height: 400.0, + ), ), - ), - new AutomaticKeepAlive( - child: new Container( - key: const GlobalObjectKey<_LeafState>(3), - height: 400.0, + new AutomaticKeepAlive( + child: new Container( + key: const GlobalObjectKey<_LeafState>(3), + height: 400.0, + ), ), - ), - ], + ], + ), )); expect(find.byKey(const GlobalObjectKey<_LeafState>(0)), findsOneWidget); expect(find.byKey(const GlobalObjectKey<_LeafState>(1)), findsOneWidget); @@ -267,38 +270,41 @@ void main() { }); testWidgets('AutomaticKeepAlive double', (WidgetTester tester) async { - await tester.pumpWidget(new ListView( - addAutomaticKeepAlives: false, - addRepaintBoundaries: false, - children: [ - new AutomaticKeepAlive( - child: new Container( - height: 400.0, - child: new Row(children: [ - new Leaf(key: const GlobalObjectKey<_LeafState>(0), child: const Placeholder()), - new Leaf(key: const GlobalObjectKey<_LeafState>(1), child: const Placeholder()), - ]), + await tester.pumpWidget(new Directionality( + textDirection: TextDirection.ltr, + child: new ListView( + addAutomaticKeepAlives: false, + addRepaintBoundaries: false, + children: [ + new AutomaticKeepAlive( + child: new Container( + height: 400.0, + child: new Row(children: [ + new Leaf(key: const GlobalObjectKey<_LeafState>(0), child: const Placeholder()), + new Leaf(key: const GlobalObjectKey<_LeafState>(1), child: const Placeholder()), + ]), + ), ), - ), - new AutomaticKeepAlive( - child: new Container( - height: 400.0, - child: new Row(children: [ - new Leaf(key: const GlobalObjectKey<_LeafState>(2), child: const Placeholder()), - new Leaf(key: const GlobalObjectKey<_LeafState>(3), child: const Placeholder()), - ]), + new AutomaticKeepAlive( + child: new Container( + height: 400.0, + child: new Row(children: [ + new Leaf(key: const GlobalObjectKey<_LeafState>(2), child: const Placeholder()), + new Leaf(key: const GlobalObjectKey<_LeafState>(3), child: const Placeholder()), + ]), + ), ), - ), - new AutomaticKeepAlive( - child: new Container( - height: 400.0, - child: new Row(children: [ - new Leaf(key: const GlobalObjectKey<_LeafState>(4), child: const Placeholder()), - new Leaf(key: const GlobalObjectKey<_LeafState>(5), child: const Placeholder()), - ]), + new AutomaticKeepAlive( + child: new Container( + height: 400.0, + child: new Row(children: [ + new Leaf(key: const GlobalObjectKey<_LeafState>(4), child: const Placeholder()), + new Leaf(key: const GlobalObjectKey<_LeafState>(5), child: const Placeholder()), + ]), + ), ), - ), - ], + ], + ), )); expect(find.byKey(const GlobalObjectKey<_LeafState>(0)), findsOneWidget); expect(find.byKey(const GlobalObjectKey<_LeafState>(1)), findsOneWidget); @@ -315,38 +321,41 @@ void main() { expect(find.byKey(const GlobalObjectKey<_LeafState>(3)), findsOneWidget); expect(find.byKey(const GlobalObjectKey<_LeafState>(4)), findsOneWidget); expect(find.byKey(const GlobalObjectKey<_LeafState>(5)), findsOneWidget); - await tester.pumpWidget(new ListView( - addAutomaticKeepAlives: false, - addRepaintBoundaries: false, - children: [ - new AutomaticKeepAlive( - child: new Container( - height: 400.0, - child: new Row(children: [ - new Leaf(key: const GlobalObjectKey<_LeafState>(1), child: const Placeholder()), - ]), + await tester.pumpWidget(new Directionality( + textDirection: TextDirection.ltr, + child: new ListView( + addAutomaticKeepAlives: false, + addRepaintBoundaries: false, + children: [ + new AutomaticKeepAlive( + child: new Container( + height: 400.0, + child: new Row(children: [ + new Leaf(key: const GlobalObjectKey<_LeafState>(1), child: const Placeholder()), + ]), + ), ), - ), - new AutomaticKeepAlive( - child: new Container( - height: 400.0, - child: new Row(children: [ - new Leaf(key: const GlobalObjectKey<_LeafState>(2), child: const Placeholder()), - new Leaf(key: const GlobalObjectKey<_LeafState>(3), child: const Placeholder()), - ]), + new AutomaticKeepAlive( + child: new Container( + height: 400.0, + child: new Row(children: [ + new Leaf(key: const GlobalObjectKey<_LeafState>(2), child: const Placeholder()), + new Leaf(key: const GlobalObjectKey<_LeafState>(3), child: const Placeholder()), + ]), + ), ), - ), - new AutomaticKeepAlive( - child: new Container( - height: 400.0, - child: new Row(children: [ - new Leaf(key: const GlobalObjectKey<_LeafState>(4), child: const Placeholder()), - new Leaf(key: const GlobalObjectKey<_LeafState>(5), child: const Placeholder()), - new Leaf(key: const GlobalObjectKey<_LeafState>(0), child: const Placeholder()), - ]), + new AutomaticKeepAlive( + child: new Container( + height: 400.0, + child: new Row(children: [ + new Leaf(key: const GlobalObjectKey<_LeafState>(4), child: const Placeholder()), + new Leaf(key: const GlobalObjectKey<_LeafState>(5), child: const Placeholder()), + new Leaf(key: const GlobalObjectKey<_LeafState>(0), child: const Placeholder()), + ]), + ), ), - ), - ], + ], + ), )); await tester.pump(); // Sometimes AutomaticKeepAlive needs an extra pump to clean things up. expect(find.byKey(const GlobalObjectKey<_LeafState>(1)), findsNothing); @@ -371,38 +380,41 @@ void main() { expect(find.byKey(const GlobalObjectKey<_LeafState>(4)), findsNothing); expect(find.byKey(const GlobalObjectKey<_LeafState>(5)), findsNothing); expect(find.byKey(const GlobalObjectKey<_LeafState>(0)), findsNothing); - await tester.pumpWidget(new ListView( - addAutomaticKeepAlives: false, - addRepaintBoundaries: false, - children: [ - new AutomaticKeepAlive( - child: new Container( - height: 400.0, - child: new Row(children: [ - new Leaf(key: const GlobalObjectKey<_LeafState>(1), child: const Placeholder()), - new Leaf(key: const GlobalObjectKey<_LeafState>(2), child: const Placeholder()), - ]), + await tester.pumpWidget(new Directionality( + textDirection: TextDirection.ltr, + child: new ListView( + addAutomaticKeepAlives: false, + addRepaintBoundaries: false, + children: [ + new AutomaticKeepAlive( + child: new Container( + height: 400.0, + child: new Row(children: [ + new Leaf(key: const GlobalObjectKey<_LeafState>(1), child: const Placeholder()), + new Leaf(key: const GlobalObjectKey<_LeafState>(2), child: const Placeholder()), + ]), + ), ), - ), - new AutomaticKeepAlive( - child: new Container( - height: 400.0, - child: new Row(children: [ - ]), + new AutomaticKeepAlive( + child: new Container( + height: 400.0, + child: new Row(children: [ + ]), + ), ), - ), - new AutomaticKeepAlive( - child: new Container( - height: 400.0, - child: new Row(children: [ - new Leaf(key: const GlobalObjectKey<_LeafState>(3), child: const Placeholder()), - new Leaf(key: const GlobalObjectKey<_LeafState>(4), child: const Placeholder()), - new Leaf(key: const GlobalObjectKey<_LeafState>(5), child: const Placeholder()), - new Leaf(key: const GlobalObjectKey<_LeafState>(0), child: const Placeholder()), - ]), + new AutomaticKeepAlive( + child: new Container( + height: 400.0, + child: new Row(children: [ + new Leaf(key: const GlobalObjectKey<_LeafState>(3), child: const Placeholder()), + new Leaf(key: const GlobalObjectKey<_LeafState>(4), child: const Placeholder()), + new Leaf(key: const GlobalObjectKey<_LeafState>(5), child: const Placeholder()), + new Leaf(key: const GlobalObjectKey<_LeafState>(0), child: const Placeholder()), + ]), + ), ), - ), - ], + ], + ), )); await tester.pump(); // Sometimes AutomaticKeepAlive needs an extra pump to clean things up. expect(find.byKey(const GlobalObjectKey<_LeafState>(1)), findsOneWidget); diff --git a/packages/flutter/test/widgets/clamp_overscrolls_test.dart b/packages/flutter/test/widgets/clamp_overscrolls_test.dart index 20a3e2cbd3..6dffdea045 100644 --- a/packages/flutter/test/widgets/clamp_overscrolls_test.dart +++ b/packages/flutter/test/widgets/clamp_overscrolls_test.dart @@ -19,6 +19,7 @@ Widget buildFrame(ScrollPhysics physics) { height: 650.0, child: new Column( crossAxisAlignment: CrossAxisAlignment.start, + textDirection: TextDirection.ltr, children: [ const SizedBox(height: 100.0, child: const Text('top')), new Expanded(child: new Container()), diff --git a/packages/flutter/test/widgets/column_test.dart b/packages/flutter/test/widgets/column_test.dart index 36b69161bd..bfe832bdf8 100644 --- a/packages/flutter/test/widgets/column_test.dart +++ b/packages/flutter/test/widgets/column_test.dart @@ -7,6 +7,8 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; void main() { + // DOWN (default) + testWidgets('Column with one flexible child', (WidgetTester tester) async { final Key columnKey = const Key('column'); final Key child0Key = const Key('child0'); @@ -386,4 +388,397 @@ void main() { expect(renderBox.size.width, equals(0.0)); expect(renderBox.size.height, equals(100.0)); }); + + + // UP + + testWidgets('Column with one flexible child', (WidgetTester tester) async { + final Key columnKey = const Key('column'); + final Key child0Key = const Key('child0'); + final Key child1Key = const Key('child1'); + final Key child2Key = const Key('child2'); + + // Default is MainAxisSize.max so the Column should be as high as the test: 600. + // Default is MainAxisAlignment.start so children so the children's + // bottom edges should be at 0, 100, 500 from bottom, child2's height should be 400. + await tester.pumpWidget(new Center( + child: new Column( + key: columnKey, + verticalDirection: VerticalDirection.up, + children: [ + new Container(key: child0Key, width: 100.0, height: 100.0), + new Expanded(child: new Container(key: child1Key, width: 100.0, height: 100.0)), + new Container(key: child2Key, width: 100.0, height: 100.0), + ] + ) + )); + + RenderBox renderBox; + BoxParentData boxParentData; + + renderBox = tester.renderObject(find.byKey(columnKey)); + expect(renderBox.size.width, equals(100.0)); + expect(renderBox.size.height, equals(600.0)); + + renderBox = tester.renderObject(find.byKey(child0Key)); + expect(renderBox.size.width, equals(100.0)); + expect(renderBox.size.height, equals(100.0)); + boxParentData = renderBox.parentData; + expect(boxParentData.offset.dy, equals(500.0)); + + renderBox = tester.renderObject(find.byKey(child1Key)); + expect(renderBox.size.width, equals(100.0)); + expect(renderBox.size.height, equals(400.0)); + boxParentData = renderBox.parentData; + expect(boxParentData.offset.dy, equals(100.0)); + + renderBox = tester.renderObject(find.byKey(child2Key)); + expect(renderBox.size.width, equals(100.0)); + expect(renderBox.size.height, equals(100.0)); + boxParentData = renderBox.parentData; + expect(boxParentData.offset.dy, equals(0.0)); + }); + + testWidgets('Column with default main axis parameters', (WidgetTester tester) async { + final Key columnKey = const Key('column'); + final Key child0Key = const Key('child0'); + final Key child1Key = const Key('child1'); + final Key child2Key = const Key('child2'); + + // Default is MainAxisSize.max so the Column should be as high as the test: 600. + // Default is MainAxisAlignment.start so children so the children's + // bottom edges should be at 0, 100, 200 from bottom + await tester.pumpWidget(new Center( + child: new Column( + key: columnKey, + verticalDirection: VerticalDirection.up, + children: [ + new Container(key: child0Key, width: 100.0, height: 100.0), + new Container(key: child1Key, width: 100.0, height: 100.0), + new Container(key: child2Key, width: 100.0, height: 100.0), + ] + ) + )); + + RenderBox renderBox; + BoxParentData boxParentData; + + renderBox = tester.renderObject(find.byKey(columnKey)); + expect(renderBox.size.width, equals(100.0)); + expect(renderBox.size.height, equals(600.0)); + + renderBox = tester.renderObject(find.byKey(child0Key)); + expect(renderBox.size.width, equals(100.0)); + expect(renderBox.size.height, equals(100.0)); + boxParentData = renderBox.parentData; + expect(boxParentData.offset.dy, equals(500.0)); + + renderBox = tester.renderObject(find.byKey(child1Key)); + expect(renderBox.size.width, equals(100.0)); + expect(renderBox.size.height, equals(100.0)); + boxParentData = renderBox.parentData; + expect(boxParentData.offset.dy, equals(400.0)); + + renderBox = tester.renderObject(find.byKey(child2Key)); + expect(renderBox.size.width, equals(100.0)); + expect(renderBox.size.height, equals(100.0)); + boxParentData = renderBox.parentData; + expect(boxParentData.offset.dy, equals(300.0)); + }); + + testWidgets('Column with MainAxisAlignment.center', (WidgetTester tester) async { + final Key columnKey = const Key('column'); + final Key child0Key = const Key('child0'); + final Key child1Key = const Key('child1'); + + // Default is MainAxisSize.max so the Column should be as high as the test: 600. + // The 100x100 children's bottom edges should be at 200, 300 from bottom + await tester.pumpWidget(new Center( + child: new Column( + key: columnKey, + mainAxisAlignment: MainAxisAlignment.center, + verticalDirection: VerticalDirection.up, + children: [ + new Container(key: child0Key, width: 100.0, height: 100.0), + new Container(key: child1Key, width: 100.0, height: 100.0), + ] + ) + )); + + RenderBox renderBox; + BoxParentData boxParentData; + + renderBox = tester.renderObject(find.byKey(columnKey)); + expect(renderBox.size.width, equals(100.0)); + expect(renderBox.size.height, equals(600.0)); + + renderBox = tester.renderObject(find.byKey(child0Key)); + expect(renderBox.size.width, equals(100.0)); + expect(renderBox.size.height, equals(100.0)); + boxParentData = renderBox.parentData; + expect(boxParentData.offset.dy, equals(300.0)); + + renderBox = tester.renderObject(find.byKey(child1Key)); + expect(renderBox.size.width, equals(100.0)); + expect(renderBox.size.height, equals(100.0)); + boxParentData = renderBox.parentData; + expect(boxParentData.offset.dy, equals(200.0)); + }); + + testWidgets('Column with MainAxisAlignment.end', (WidgetTester tester) async { + final Key columnKey = const Key('column'); + final Key child0Key = const Key('child0'); + final Key child1Key = const Key('child1'); + final Key child2Key = const Key('child2'); + + // Default is MainAxisSize.max so the Column should be as high as the test: 600. + // The 100x100 children's bottom edges should be at 300, 400, 500 from bottom. + await tester.pumpWidget(new Center( + child: new Column( + key: columnKey, + mainAxisAlignment: MainAxisAlignment.end, + verticalDirection: VerticalDirection.up, + children: [ + new Container(key: child0Key, width: 100.0, height: 100.0), + new Container(key: child1Key, width: 100.0, height: 100.0), + new Container(key: child2Key, width: 100.0, height: 100.0), + ] + ) + )); + + RenderBox renderBox; + BoxParentData boxParentData; + + renderBox = tester.renderObject(find.byKey(columnKey)); + expect(renderBox.size.width, equals(100.0)); + expect(renderBox.size.height, equals(600.0)); + + renderBox = tester.renderObject(find.byKey(child0Key)); + expect(renderBox.size.width, equals(100.0)); + expect(renderBox.size.height, equals(100.0)); + boxParentData = renderBox.parentData; + expect(boxParentData.offset.dy, equals(200.0)); + + renderBox = tester.renderObject(find.byKey(child1Key)); + expect(renderBox.size.width, equals(100.0)); + expect(renderBox.size.height, equals(100.0)); + boxParentData = renderBox.parentData; + expect(boxParentData.offset.dy, equals(100.0)); + + renderBox = tester.renderObject(find.byKey(child2Key)); + expect(renderBox.size.width, equals(100.0)); + expect(renderBox.size.height, equals(100.0)); + boxParentData = renderBox.parentData; + expect(boxParentData.offset.dy, equals(0.0)); + }); + + testWidgets('Column with MainAxisAlignment.spaceBetween', (WidgetTester tester) async { + final Key columnKey = const Key('column'); + final Key child0Key = const Key('child0'); + final Key child1Key = const Key('child1'); + final Key child2Key = const Key('child2'); + + // Default is MainAxisSize.max so the Column should be as high as the test: 600. + // The 100x100 children's bottom edges should be at 0, 250, 500 from bottom + await tester.pumpWidget(new Center( + child: new Column( + key: columnKey, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + verticalDirection: VerticalDirection.up, + children: [ + new Container(key: child0Key, width: 100.0, height: 100.0), + new Container(key: child1Key, width: 100.0, height: 100.0), + new Container(key: child2Key, width: 100.0, height: 100.0), + ] + ) + )); + + RenderBox renderBox; + BoxParentData boxParentData; + + renderBox = tester.renderObject(find.byKey(columnKey)); + expect(renderBox.size.width, equals(100.0)); + expect(renderBox.size.height, equals(600.0)); + + renderBox = tester.renderObject(find.byKey(child0Key)); + expect(renderBox.size.width, equals(100.0)); + expect(renderBox.size.height, equals(100.0)); + boxParentData = renderBox.parentData; + expect(boxParentData.offset.dy, equals(500.0)); + + renderBox = tester.renderObject(find.byKey(child1Key)); + expect(renderBox.size.width, equals(100.0)); + expect(renderBox.size.height, equals(100.0)); + boxParentData = renderBox.parentData; + expect(boxParentData.offset.dy, equals(250.0)); + + renderBox = tester.renderObject(find.byKey(child2Key)); + expect(renderBox.size.width, equals(100.0)); + expect(renderBox.size.height, equals(100.0)); + boxParentData = renderBox.parentData; + expect(boxParentData.offset.dy, equals(0.0)); + }); + + testWidgets('Column with MainAxisAlignment.spaceAround', (WidgetTester tester) async { + final Key columnKey = const Key('column'); + final Key child0Key = const Key('child0'); + final Key child1Key = const Key('child1'); + final Key child2Key = const Key('child2'); + final Key child3Key = const Key('child3'); + + // Default is MainAxisSize.max so the Column should be as high as the test: 600. + // The 100x100 children's bottom edges should be at 25, 175, 325, 475 from bottom + await tester.pumpWidget(new Center( + child: new Column( + key: columnKey, + mainAxisAlignment: MainAxisAlignment.spaceAround, + verticalDirection: VerticalDirection.up, + children: [ + new Container(key: child0Key, width: 100.0, height: 100.0), + new Container(key: child1Key, width: 100.0, height: 100.0), + new Container(key: child2Key, width: 100.0, height: 100.0), + new Container(key: child3Key, width: 100.0, height: 100.0), + ] + ) + )); + + RenderBox renderBox; + BoxParentData boxParentData; + + renderBox = tester.renderObject(find.byKey(columnKey)); + expect(renderBox.size.width, equals(100.0)); + expect(renderBox.size.height, equals(600.0)); + + renderBox = tester.renderObject(find.byKey(child0Key)); + expect(renderBox.size.width, equals(100.0)); + expect(renderBox.size.height, equals(100.0)); + boxParentData = renderBox.parentData; + expect(boxParentData.offset.dy, equals(500.0 - 25.0)); + + renderBox = tester.renderObject(find.byKey(child1Key)); + expect(renderBox.size.width, equals(100.0)); + expect(renderBox.size.height, equals(100.0)); + boxParentData = renderBox.parentData; + expect(boxParentData.offset.dy, equals(500.0 - 175.0)); + + renderBox = tester.renderObject(find.byKey(child2Key)); + expect(renderBox.size.width, equals(100.0)); + expect(renderBox.size.height, equals(100.0)); + boxParentData = renderBox.parentData; + expect(boxParentData.offset.dy, equals(500.0 - 325.0)); + + renderBox = tester.renderObject(find.byKey(child3Key)); + expect(renderBox.size.width, equals(100.0)); + expect(renderBox.size.height, equals(100.0)); + boxParentData = renderBox.parentData; + expect(boxParentData.offset.dy, equals(500.0 - 475.0)); + }); + + testWidgets('Column with MainAxisAlignment.spaceEvenly', (WidgetTester tester) async { + final Key columnKey = const Key('column'); + final Key child0Key = const Key('child0'); + final Key child1Key = const Key('child1'); + final Key child2Key = const Key('child2'); + + // Default is MainAxisSize.max so the Column should be as high as the test: 600. + // The 100x20 children's bottom edges should be at 135, 290, 445 from bottom + await tester.pumpWidget(new Center( + child: new Column( + key: columnKey, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + verticalDirection: VerticalDirection.up, + children: [ + new Container(key: child0Key, width: 100.0, height: 20.0), + new Container(key: child1Key, width: 100.0, height: 20.0), + new Container(key: child2Key, width: 100.0, height: 20.0), + ] + ) + )); + + RenderBox renderBox; + BoxParentData boxParentData; + + renderBox = tester.renderObject(find.byKey(columnKey)); + expect(renderBox.size.width, equals(100.0)); + expect(renderBox.size.height, equals(600.0)); + + renderBox = tester.renderObject(find.byKey(child0Key)); + expect(renderBox.size.width, equals(100.0)); + expect(renderBox.size.height, equals(20.0)); + boxParentData = renderBox.parentData; + expect(boxParentData.offset.dy, equals(600.0 - 135.0 - 20.0)); + + renderBox = tester.renderObject(find.byKey(child1Key)); + expect(renderBox.size.width, equals(100.0)); + expect(renderBox.size.height, equals(20.0)); + boxParentData = renderBox.parentData; + expect(boxParentData.offset.dy, equals(600.0 - 290.0 - 20.0)); + + renderBox = tester.renderObject(find.byKey(child2Key)); + expect(renderBox.size.width, equals(100.0)); + expect(renderBox.size.height, equals(20.0)); + boxParentData = renderBox.parentData; + expect(boxParentData.offset.dy, equals(600.0 - 445.0 - 20.0)); + }); + + testWidgets('Column and MainAxisSize.min', (WidgetTester tester) async { + final Key flexKey = const Key('flexKey'); + + // Default is MainAxisSize.max so the Column should be as high as the test: 600. + await tester.pumpWidget(new Center( + child: new Column( + key: flexKey, + verticalDirection: VerticalDirection.up, + children: [ + new Container(width: 100.0, height: 100.0), + new Container(width: 100.0, height: 150.0) + ] + ) + )); + RenderBox renderBox = tester.renderObject(find.byKey(flexKey)); + expect(renderBox.size.width, equals(100.0)); + expect(renderBox.size.height, equals(600.0)); + + // Column with MainAxisSize.min without flexible children shrink wraps. + await tester.pumpWidget(new Center( + child: new Column( + key: flexKey, + mainAxisSize: MainAxisSize.min, + verticalDirection: VerticalDirection.up, + children: [ + new Container(width: 100.0, height: 100.0), + new Container(width: 100.0, height: 150.0) + ] + ) + )); + renderBox = tester.renderObject(find.byKey(flexKey)); + expect(renderBox.size.width, equals(100.0)); + expect(renderBox.size.height, equals(250.0)); + }); + + testWidgets('Column MainAxisSize.min layout at zero size', (WidgetTester tester) async { + final Key childKey = const Key('childKey'); + + await tester.pumpWidget(new Center( + child: new Container( + width: 0.0, + height: 0.0, + child: new Column( + mainAxisSize: MainAxisSize.min, + verticalDirection: VerticalDirection.up, + children: [ + new Container( + key: childKey, + width: 100.0, + height: 100.0 + ) + ] + ) + ) + )); + + final RenderBox renderBox = tester.renderObject(find.byKey(childKey)); + expect(renderBox.size.width, equals(0.0)); + expect(renderBox.size.height, equals(100.0)); + }); } diff --git a/packages/flutter/test/widgets/container_test.dart b/packages/flutter/test/widgets/container_test.dart index a29f72cd56..65b49fbc56 100644 --- a/packages/flutter/test/widgets/container_test.dart +++ b/packages/flutter/test/widgets/container_test.dart @@ -59,7 +59,8 @@ void main() { ' │ parentData: offset=Offset(0.0, 0.0) (can use size)\n' ' │ constraints: BoxConstraints(0.0<=w<=800.0, 0.0<=h<=600.0)\n' ' │ size: Size(63.0, 88.0)\n' - ' │ padding: EdgeInsets(5.0, 5.0, 5.0, 5.0)\n' + ' │ padding: EdgeInsets.all(5.0)\n' + ' │ textDirection: null\n' ' │\n' ' └─child: RenderConstrainedBox#00000 relayoutBoundary=up2\n' ' │ creator: ConstrainedBox ← Padding ← Container ← Align ← [root]\n' @@ -98,7 +99,8 @@ void main() { ' │ parentData: (can use size)\n' ' │ constraints: BoxConstraints(w=53.0, h=78.0)\n' ' │ size: Size(53.0, 78.0)\n' - ' │ padding: EdgeInsets(7.0, 7.0, 7.0, 7.0)\n' + ' │ padding: EdgeInsets.all(7.0)\n' + ' │ textDirection: null\n' ' │\n' ' └─child: RenderPositionedBox#00000\n' ' │ creator: Align ← Padding ← DecoratedBox ← DecoratedBox ←\n' @@ -106,7 +108,7 @@ void main() { ' │ parentData: offset=Offset(7.0, 7.0) (can use size)\n' ' │ constraints: BoxConstraints(w=39.0, h=64.0)\n' ' │ size: Size(39.0, 64.0)\n' - ' │ alignment: FractionalOffset(1.0, 1.0)\n' + ' │ alignment: FractionalOffset.bottomRight\n' ' │ widthFactor: expand\n' ' │ heightFactor: expand\n' ' │\n' diff --git a/packages/flutter/test/widgets/flex_test.dart b/packages/flutter/test/widgets/flex_test.dart index 147ec58382..126e5efa30 100644 --- a/packages/flutter/test/widgets/flex_test.dart +++ b/packages/flutter/test/widgets/flex_test.dart @@ -47,6 +47,7 @@ void main() { testWidgets('Flexible defaults to loose', (WidgetTester tester) async { await tester.pumpWidget( new Row( + textDirection: TextDirection.ltr, children: [ const Flexible(child: const SizedBox(width: 100.0, height: 200.0)), ], @@ -60,6 +61,7 @@ void main() { testWidgets('Can pass null for flex', (WidgetTester tester) async { await tester.pumpWidget( new Row( + textDirection: TextDirection.ltr, children: [ const Expanded(flex: null, child: const Text('one')), const Flexible(flex: null, child: const Text('two')), diff --git a/packages/flutter/test/widgets/focus_test.dart b/packages/flutter/test/widgets/focus_test.dart index 8ec6ca6838..282d1f20fc 100644 --- a/packages/flutter/test/widgets/focus_test.dart +++ b/packages/flutter/test/widgets/focus_test.dart @@ -145,6 +145,7 @@ void main() { node: parentFocusScope, autofocus: true, child: new Row( + textDirection: TextDirection.ltr, children: [ const TestFocusable( no: 'a', @@ -193,6 +194,7 @@ void main() { new FocusScope( node: parentFocusScope, child: new Row( + textDirection: TextDirection.ltr, children: [ const TestFocusable( no: 'a', @@ -218,6 +220,7 @@ void main() { new FocusScope( node: parentFocusScope, child: new Row( + textDirection: TextDirection.ltr, children: [ const TestFocusable( no: 'a', diff --git a/packages/flutter/test/widgets/framework_test.dart b/packages/flutter/test/widgets/framework_test.dart index df973062b5..bfa0119cc8 100644 --- a/packages/flutter/test/widgets/framework_test.dart +++ b/packages/flutter/test/widgets/framework_test.dart @@ -486,7 +486,7 @@ void main() { expect( element.toStringDeep(), equalsIgnoringHashCodes( - 'Column-[GlobalKey#00000](renderObject: RenderFlex#00000)\n' + 'Column-[GlobalKey#00000](direction: vertical, mainAxisAlignment: start, crossAxisAlignment: center, renderObject: RenderFlex#00000)\n' '├Container\n' '│└LimitedBox(maxWidth: 0.0, maxHeight: 0.0, renderObject: RenderLimitedBox#00000 relayoutBoundary=up1)\n' '│ └ConstrainedBox(BoxConstraints(biggest), renderObject: RenderConstrainedBox#00000 relayoutBoundary=up2)\n' diff --git a/packages/flutter/test/widgets/global_keys_duplicated_test.dart b/packages/flutter/test/widgets/global_keys_duplicated_test.dart index 442553af9a..7a5e55c5e2 100644 --- a/packages/flutter/test/widgets/global_keys_duplicated_test.dart +++ b/packages/flutter/test/widgets/global_keys_duplicated_test.dart @@ -24,10 +24,13 @@ void main() { }); testWidgets('GlobalKey children of two nodes', (WidgetTester tester) async { - await tester.pumpWidget(new Row(children: [ - new Container(child: new Container(key: const GlobalObjectKey(0))), - new Container(child: new Container(key: const GlobalObjectKey(0))), - ])); + await tester.pumpWidget(new Row( + textDirection: TextDirection.ltr, + children: [ + new Container(child: new Container(key: const GlobalObjectKey(0))), + new Container(child: new Container(key: const GlobalObjectKey(0))), + ], + )); final dynamic error = tester.takeException(); expect(error, isFlutterError); expect(error.toString(), startsWith('Multiple widgets used the same GlobalKey.\n')); @@ -38,10 +41,13 @@ void main() { }); testWidgets('GlobalKey children of two different nodes', (WidgetTester tester) async { - await tester.pumpWidget(new Row(children: [ - new Container(child: new Container(key: const GlobalObjectKey(0))), - new Container(key: const Key('x'), child: new Container(key: const GlobalObjectKey(0))), - ])); + await tester.pumpWidget(new Row( + textDirection: TextDirection.ltr, + children: [ + new Container(child: new Container(key: const GlobalObjectKey(0))), + new Container(key: const Key('x'), child: new Container(key: const GlobalObjectKey(0))), + ], + )); final dynamic error = tester.takeException(); expect(error, isFlutterError); expect(error.toString(), startsWith('Multiple widgets used the same GlobalKey.\n')); @@ -55,17 +61,20 @@ void main() { testWidgets('GlobalKey children of two nodes', (WidgetTester tester) async { StateSetter nestedSetState; bool flag = false; - await tester.pumpWidget(new Row(children: [ - new Container(child: new Container(key: const GlobalObjectKey(0))), - new Container(child: new StatefulBuilder( - builder: (BuildContext context, StateSetter setState) { - nestedSetState = setState; - if (flag) - return new Container(key: const GlobalObjectKey(0)); - return new Container(); - }, - )), - ])); + await tester.pumpWidget(new Row( + textDirection: TextDirection.ltr, + children: [ + new Container(child: new Container(key: const GlobalObjectKey(0))), + new Container(child: new StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + nestedSetState = setState; + if (flag) + return new Container(key: const GlobalObjectKey(0)); + return new Container(); + }, + )), + ], + )); nestedSetState(() { flag = true; }); await tester.pump(); final dynamic error = tester.takeException(); diff --git a/packages/flutter/test/widgets/image_test.dart b/packages/flutter/test/widgets/image_test.dart index 8c9be1fba3..7a487d8418 100644 --- a/packages/flutter/test/widgets/image_test.dart +++ b/packages/flutter/test/widgets/image_test.dart @@ -235,6 +235,7 @@ void main() { // of the Image changes and the MediaQuery widgets do not. await tester.pumpWidget( new Row( + textDirection: TextDirection.ltr, children: [ new MediaQuery( key: mediaQueryKey2, @@ -263,6 +264,7 @@ void main() { await tester.pumpWidget( new Row( + textDirection: TextDirection.ltr, children: [ new MediaQuery( key: mediaQueryKey2, diff --git a/packages/flutter/test/widgets/layout_builder_and_parent_data_test.dart b/packages/flutter/test/widgets/layout_builder_and_parent_data_test.dart index 7f25030689..22929b532e 100644 --- a/packages/flutter/test/widgets/layout_builder_and_parent_data_test.dart +++ b/packages/flutter/test/widgets/layout_builder_and_parent_data_test.dart @@ -29,6 +29,7 @@ class SizeChangerState extends State { @override Widget build(BuildContext context) { return new Row( + textDirection: TextDirection.ltr, children: [ new SizedBox( height: _flag ? 50.0 : 100.0, diff --git a/packages/flutter/test/widgets/layout_builder_mutations_test.dart b/packages/flutter/test/widgets/layout_builder_mutations_test.dart index 1eff184584..f0d238fec2 100644 --- a/packages/flutter/test/widgets/layout_builder_mutations_test.dart +++ b/packages/flutter/test/widgets/layout_builder_mutations_test.dart @@ -25,38 +25,44 @@ void main() { testWidgets('Moving a global key from another LayoutBuilder at layout time', (WidgetTester tester) async { final GlobalKey victimKey = new GlobalKey(); - await tester.pumpWidget(new Row(children: [ - new Wrapper( - child: new LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) { - return const SizedBox(); - }), - ), - new Wrapper( - child: new Wrapper( + await tester.pumpWidget(new Row( + textDirection: TextDirection.ltr, + children: [ + new Wrapper( + child: new LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) { + return const SizedBox(); + }), + ), + new Wrapper( + child: new Wrapper( + child: new LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) { + return new Wrapper( + child: new SizedBox(key: victimKey) + ); + }) + ) + ), + ], + )); + + await tester.pumpWidget(new Row( + textDirection: TextDirection.ltr, + children: [ + new Wrapper( child: new LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) { return new Wrapper( child: new SizedBox(key: victimKey) ); }) - ) - ), - ])); - - await tester.pumpWidget(new Row(children: [ - new Wrapper( - child: new LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) { - return new Wrapper( - child: new SizedBox(key: victimKey) - ); - }) - ), - new Wrapper( - child: new Wrapper( - child: new LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) { - return const SizedBox(); - }) - ) - ), - ])); + ), + new Wrapper( + child: new Wrapper( + child: new LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) { + return const SizedBox(); + }) + ) + ), + ], + )); }); } diff --git a/packages/flutter/test/widgets/localizations_test.dart b/packages/flutter/test/widgets/localizations_test.dart index ac718ae19e..b67f1fd827 100644 --- a/packages/flutter/test/widgets/localizations_test.dart +++ b/packages/flutter/test/widgets/localizations_test.dart @@ -42,6 +42,9 @@ class SyncTestLocalizationsDelegate extends LocalizationsDelegate '$runtimeType($prefix)'; } class AsyncTestLocalizationsDelegate extends LocalizationsDelegate { @@ -58,6 +61,9 @@ class AsyncTestLocalizationsDelegate extends LocalizationsDelegate '$runtimeType($prefix)'; } class MoreLocalizations { @@ -289,6 +295,7 @@ void main() { locale: const Locale('en', 'GB'), delegates: >[ new SyncTestLocalizationsDelegate(), + new DefaultWidgetsLocalizationsDelegate(), ], // Create a new context within the en_GB Localization child: new Builder( @@ -359,6 +366,7 @@ void main() { expect(find.text('A: ---en_US'), findsOneWidget); expect(find.text('B: en_US'), findsOneWidget); expect(modifiedDelegate.shouldReloadValues, [true]); + expect(originalDelegate.shouldReloadValues, []); }); testWidgets('Localizations async delegate shouldReload returns true', (WidgetTester tester) async { @@ -410,3 +418,16 @@ void main() { }); } + +// Same as _WidgetsLocalizationsDelegate in widgets/app.dart +class DefaultWidgetsLocalizationsDelegate extends LocalizationsDelegate { + const DefaultWidgetsLocalizationsDelegate(); + + @override + Future load(Locale locale) { + return new SynchronousFuture(const WidgetsLocalizations()); + } + + @override + bool shouldReload(DefaultWidgetsLocalizationsDelegate old) => false; +} diff --git a/packages/flutter/test/widgets/nested_scroll_view_test.dart b/packages/flutter/test/widgets/nested_scroll_view_test.dart index a46965489b..8d1970b670 100644 --- a/packages/flutter/test/widgets/nested_scroll_view_test.dart +++ b/packages/flutter/test/widgets/nested_scroll_view_test.dart @@ -23,71 +23,74 @@ class _CustomPhysics extends ClampingScrollPhysics { Widget buildTest({ ScrollController controller, String title:'TTTTTTTT' }) { return new MediaQuery( data: const MediaQueryData(), - child: new Scaffold( - body: new DefaultTabController( - length: 4, - child: new NestedScrollView( - controller: controller, - headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { - return [ - new SliverAppBar( - title: new Text(title), - pinned: true, - expandedHeight: 200.0, - forceElevated: innerBoxIsScrolled, - bottom: new TabBar( - tabs: const [ - const Tab(text: 'AA'), - const Tab(text: 'BB'), - const Tab(text: 'CC'), - const Tab(text: 'DD'), + child: new Directionality( + textDirection: TextDirection.ltr, + child: new Scaffold( + body: new DefaultTabController( + length: 4, + child: new NestedScrollView( + controller: controller, + headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { + return [ + new SliverAppBar( + title: new Text(title), + pinned: true, + expandedHeight: 200.0, + forceElevated: innerBoxIsScrolled, + bottom: new TabBar( + tabs: const [ + const Tab(text: 'AA'), + const Tab(text: 'BB'), + const Tab(text: 'CC'), + const Tab(text: 'DD'), + ], + ), + ), + ]; + }, + body: new TabBarView( + children: [ + new ListView( + children: [ + new Container( + height: 300.0, + child: const Text('aaa1'), + ), + new Container( + height: 200.0, + child: const Text('aaa2'), + ), + new Container( + height: 100.0, + child: const Text('aaa3'), + ), + new Container( + height: 50.0, + child: const Text('aaa4'), + ), ], ), - ), - ]; - }, - body: new TabBarView( - children: [ - new ListView( - children: [ - new Container( - height: 300.0, - child: const Text('aaa1'), - ), - new Container( - height: 200.0, - child: const Text('aaa2'), - ), - new Container( - height: 100.0, - child: const Text('aaa3'), - ), - new Container( - height: 50.0, - child: const Text('aaa4'), - ), - ], - ), - new ListView( - children: [ - new Container( - height: 100.0, - child: const Text('bbb1'), - ), - ], - ), - new Container( - child: const Center(child: const Text('ccc1')), - ), - new ListView( - children: [ - new Container( - height: 10000.0, - child: const Text('ddd1'), - ), - ], - ), - ], + new ListView( + children: [ + new Container( + height: 100.0, + child: const Text('bbb1'), + ), + ], + ), + new Container( + child: const Center(child: const Text('ccc1')), + ), + new ListView( + children: [ + new Container( + height: 10000.0, + child: const Text('ddd1'), + ), + ], + ), + ], + ), ), ), ), diff --git a/packages/flutter/test/widgets/overflow_box_test.dart b/packages/flutter/test/widgets/overflow_box_test.dart index b40b9385a9..7c7d384765 100644 --- a/packages/flutter/test/widgets/overflow_box_test.dart +++ b/packages/flutter/test/widgets/overflow_box_test.dart @@ -42,7 +42,7 @@ void main() { .where((DiagnosticsNode n) => !n.hidden) .map((DiagnosticsNode n) => n.toString()).toList(); expect(description, [ - 'alignment: FractionalOffset(0.5, 0.5)', + 'alignment: FractionalOffset.center', 'minWidth: 1.0', 'maxWidth: 2.0', 'minHeight: 3.0', diff --git a/packages/flutter/test/widgets/overlay_test.dart b/packages/flutter/test/widgets/overlay_test.dart index aa28b8960b..9c1e3df754 100644 --- a/packages/flutter/test/widgets/overlay_test.dart +++ b/packages/flutter/test/widgets/overlay_test.dart @@ -43,7 +43,7 @@ void main() { ' ╎ │ size)\n' ' ╎ │ constraints: BoxConstraints(w=800.0, h=600.0)\n' ' ╎ │ size: Size(800.0, 600.0)\n' - ' ╎ │ alignment: FractionalOffset(0.0, 0.0)\n' + ' ╎ │ alignment: FractionalOffset.topLeft\n' ' ╎ │ fit: expand\n' ' ╎ │ overflow: clip\n' ' ╎ │\n' @@ -112,7 +112,7 @@ void main() { ' ╎ │ size)\n' ' ╎ │ constraints: BoxConstraints(w=800.0, h=600.0)\n' ' ╎ │ size: Size(800.0, 600.0)\n' - ' ╎ │ alignment: FractionalOffset(0.0, 0.0)\n' + ' ╎ │ alignment: FractionalOffset.topLeft\n' ' ╎ │ fit: expand\n' ' ╎ │ overflow: clip\n' ' ╎ │\n' diff --git a/packages/flutter/test/widgets/reparent_state_test.dart b/packages/flutter/test/widgets/reparent_state_test.dart index 6f3ed6d0fa..b5cd71251c 100644 --- a/packages/flutter/test/widgets/reparent_state_test.dart +++ b/packages/flutter/test/widgets/reparent_state_test.dart @@ -335,6 +335,7 @@ void main() { await tester.pumpWidget( new Row( + textDirection: TextDirection.ltr, children: [ new StateMarker( key: key1, @@ -352,6 +353,7 @@ void main() { await tester.pumpWidget( new Row( + textDirection: TextDirection.ltr, children: [ new StateMarker( key: key2, diff --git a/packages/flutter/test/widgets/rotated_box_test.dart b/packages/flutter/test/widgets/rotated_box_test.dart index 65b0e3f5ea..3e003caa58 100644 --- a/packages/flutter/test/widgets/rotated_box_test.dart +++ b/packages/flutter/test/widgets/rotated_box_test.dart @@ -16,6 +16,7 @@ void main() { key: rotatedBoxKey, quarterTurns: 1, child: new Row( + textDirection: TextDirection.ltr, mainAxisSize: MainAxisSize.min, children: [ new GestureDetector( diff --git a/packages/flutter/test/widgets/row_test.dart b/packages/flutter/test/widgets/row_test.dart index c2b21ab1e5..1220e5d58d 100644 --- a/packages/flutter/test/widgets/row_test.dart +++ b/packages/flutter/test/widgets/row_test.dart @@ -2,12 +2,316 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; + +class OrderPainter extends CustomPainter { + const OrderPainter(this.index); + + final int index; + + static List log = []; + + @override + void paint(Canvas canvas, Size size) { + log.add(index); + } + + @override + bool shouldRepaint(OrderPainter old) => false; +} + +Widget log(int index) => new CustomPaint(painter: new OrderPainter(index)); void main() { - testWidgets('Row with one Flexible child', (WidgetTester tester) async { + // NO DIRECTION + + testWidgets('Row with one Flexible child - no textDirection', (WidgetTester tester) async { + OrderPainter.log.clear(); + final Key rowKey = const Key('row'); + final Key child0Key = const Key('child0'); + final Key child1Key = const Key('child1'); + final Key child2Key = const Key('child2'); + + final FlutterExceptionHandler oldHandler = FlutterError.onError; + dynamic exception; + FlutterError.onError = (FlutterErrorDetails details) { + exception ??= details.exception; + }; + + // Default is MainAxisAlignment.start so this should fail, asking for a direction. + await tester.pumpWidget(new Center( + child: new Row( + key: rowKey, + children: [ + new Container(key: child0Key, width: 100.0, height: 100.0, child: log(1)), + new Expanded(child: new Container(key: child1Key, width: 100.0, height: 100.0, child: log(2))), + new Container(key: child2Key, width: 100.0, height: 100.0, child: log(3)), + ], + ), + )); + + FlutterError.onError = oldHandler; + expect(exception, isAssertionError); + expect(exception.toString(), contains('textDirection')); + expect(OrderPainter.log, []); + }); + + testWidgets('Row with default main axis parameters - no textDirection', (WidgetTester tester) async { + OrderPainter.log.clear(); + final Key rowKey = const Key('row'); + final Key child0Key = const Key('child0'); + final Key child1Key = const Key('child1'); + final Key child2Key = const Key('child2'); + + final FlutterExceptionHandler oldHandler = FlutterError.onError; + dynamic exception; + FlutterError.onError = (FlutterErrorDetails details) { + exception ??= details.exception; + }; + + // Default is MainAxisAlignment.start so this should fail too. + await tester.pumpWidget(new Center( + child: new Row( + key: rowKey, + children: [ + new Container(key: child0Key, width: 100.0, height: 100.0, child: log(1)), + new Container(key: child1Key, width: 100.0, height: 100.0, child: log(2)), + new Container(key: child2Key, width: 100.0, height: 100.0, child: log(3)), + ], + ), + )); + + FlutterError.onError = oldHandler; + expect(exception, isAssertionError); + expect(exception.toString(), contains('textDirection')); + expect(OrderPainter.log, []); + }); + + testWidgets('Row with MainAxisAlignment.center - no textDirection', (WidgetTester tester) async { + OrderPainter.log.clear(); + final Key rowKey = const Key('row'); + final Key child0Key = const Key('child0'); + final Key child1Key = const Key('child1'); + + final FlutterExceptionHandler oldHandler = FlutterError.onError; + dynamic exception; + FlutterError.onError = (FlutterErrorDetails details) { + exception ??= details.exception; + }; + + // More than one child, so it's not clear what direction to lay out in: should fail. + await tester.pumpWidget(new Center( + child: new Row( + key: rowKey, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + new Container(key: child0Key, width: 100.0, height: 100.0, child: log(1)), + new Container(key: child1Key, width: 100.0, height: 100.0, child: log(2)), + ], + ), + )); + + FlutterError.onError = oldHandler; + expect(exception, isAssertionError); + expect(exception.toString(), contains('textDirection')); + expect(OrderPainter.log, []); + }); + + testWidgets('Row with MainAxisAlignment.end - no textDirection', (WidgetTester tester) async { + OrderPainter.log.clear(); + final Key rowKey = const Key('row'); + final Key child0Key = const Key('child0'); + final Key child1Key = const Key('child1'); + final Key child2Key = const Key('child2'); + + final FlutterExceptionHandler oldHandler = FlutterError.onError; + dynamic exception; + FlutterError.onError = (FlutterErrorDetails details) { + exception ??= details.exception; + }; + + // No direction so this should fail, asking for a direction. + await tester.pumpWidget(new Center( + child: new Row( + key: rowKey, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + new Container(key: child0Key, width: 100.0, height: 100.0, child: log(1)), + new Container(key: child1Key, width: 100.0, height: 100.0, child: log(2)), + new Container(key: child2Key, width: 100.0, height: 100.0, child: log(3)), + ], + ), + )); + + FlutterError.onError = oldHandler; + expect(exception, isAssertionError); + expect(exception.toString(), contains('textDirection')); + expect(OrderPainter.log, []); + }); + + testWidgets('Row with MainAxisAlignment.spaceBetween - no textDirection', (WidgetTester tester) async { + OrderPainter.log.clear(); + final Key rowKey = const Key('row'); + final Key child0Key = const Key('child0'); + final Key child1Key = const Key('child1'); + final Key child2Key = const Key('child2'); + + final FlutterExceptionHandler oldHandler = FlutterError.onError; + dynamic exception; + FlutterError.onError = (FlutterErrorDetails details) { + exception ??= details.exception; + }; + + // More than one child, so it's not clear what direction to lay out in: should fail. + await tester.pumpWidget(new Center( + child: new Row( + key: rowKey, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + new Container(key: child0Key, width: 100.0, height: 100.0, child: log(1)), + new Container(key: child1Key, width: 100.0, height: 100.0, child: log(2)), + new Container(key: child2Key, width: 100.0, height: 100.0, child: log(3)), + ], + ), + )); + + FlutterError.onError = oldHandler; + expect(exception, isAssertionError); + expect(exception.toString(), contains('textDirection')); + expect(OrderPainter.log, []); + }); + + testWidgets('Row with MainAxisAlignment.spaceAround - no textDirection', (WidgetTester tester) async { + OrderPainter.log.clear(); + final Key rowKey = const Key('row'); + final Key child0Key = const Key('child0'); + final Key child1Key = const Key('child1'); + final Key child2Key = const Key('child2'); + final Key child3Key = const Key('child3'); + + final FlutterExceptionHandler oldHandler = FlutterError.onError; + dynamic exception; + FlutterError.onError = (FlutterErrorDetails details) { + exception ??= details.exception; + }; + + // More than one child, so it's not clear what direction to lay out in: should fail. + await tester.pumpWidget(new Center( + child: new Row( + key: rowKey, + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + new Container(key: child0Key, width: 100.0, height: 100.0, child: log(1)), + new Container(key: child1Key, width: 100.0, height: 100.0, child: log(2)), + new Container(key: child2Key, width: 100.0, height: 100.0, child: log(3)), + new Container(key: child3Key, width: 100.0, height: 100.0, child: log(4)), + ], + ), + )); + + FlutterError.onError = oldHandler; + expect(exception, isAssertionError); + expect(exception.toString(), contains('textDirection')); + expect(OrderPainter.log, []); + }); + + testWidgets('Row with MainAxisAlignment.spaceEvenly - no textDirection', (WidgetTester tester) async { + OrderPainter.log.clear(); + final Key rowKey = const Key('row'); + final Key child0Key = const Key('child0'); + final Key child1Key = const Key('child1'); + final Key child2Key = const Key('child2'); + + final FlutterExceptionHandler oldHandler = FlutterError.onError; + dynamic exception; + FlutterError.onError = (FlutterErrorDetails details) { + exception ??= details.exception; + }; + + // More than one child, so it's not clear what direction to lay out in: should fail. + await tester.pumpWidget(new Center( + child: new Row( + key: rowKey, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + new Container(key: child0Key, width: 200.0, height: 100.0, child: log(1)), + new Container(key: child1Key, width: 200.0, height: 100.0, child: log(2)), + new Container(key: child2Key, width: 200.0, height: 100.0, child: log(3)), + ], + ), + )); + + FlutterError.onError = oldHandler; + expect(exception, isAssertionError); + expect(exception.toString(), contains('textDirection')); + expect(OrderPainter.log, []); + }); + + testWidgets('Row and MainAxisSize.min - no textDirection', (WidgetTester tester) async { + OrderPainter.log.clear(); + final Key rowKey = const Key('rowKey'); + final Key child0Key = const Key('child0'); + final Key child1Key = const Key('child1'); + + final FlutterExceptionHandler oldHandler = FlutterError.onError; + dynamic exception; + FlutterError.onError = (FlutterErrorDetails details) { + exception ??= details.exception; + }; + + // Default is MainAxisAlignment.start so this should fail, asking for a direction. + await tester.pumpWidget(new Center( + child: new Row( + key: rowKey, + mainAxisSize: MainAxisSize.min, + children: [ + new Container(key: child0Key, width: 100.0, height: 100.0, child: log(1)), + new Container(key: child1Key, width: 150.0, height: 100.0, child: log(2)), + ], + ), + )); + + FlutterError.onError = oldHandler; + expect(exception, isAssertionError); + expect(exception.toString(), contains('textDirection')); + expect(OrderPainter.log, []); + }); + + testWidgets('Row MainAxisSize.min layout at zero size - no textDirection', (WidgetTester tester) async { + OrderPainter.log.clear(); + final Key childKey = const Key('childKey'); + + await tester.pumpWidget(new Center( + child: new Container( + width: 0.0, + height: 0.0, + child: new Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + new Container( + key: childKey, + width: 100.0, + height: 100.0, + ), + ], + ), + ), + )); + + final RenderBox renderBox = tester.renderObject(find.byKey(childKey)); + expect(renderBox.size.width, equals(100.0)); + expect(renderBox.size.height, equals(0.0)); + }); + + + // LTR + + testWidgets('Row with one Flexible child - LTR', (WidgetTester tester) async { + OrderPainter.log.clear(); final Key rowKey = const Key('row'); final Key child0Key = const Key('child0'); final Key child1Key = const Key('child1'); @@ -19,12 +323,13 @@ void main() { await tester.pumpWidget(new Center( child: new Row( key: rowKey, + textDirection: TextDirection.ltr, children: [ - new Container(key: child0Key, width: 100.0, height: 100.0), - new Expanded(child: new Container(key: child1Key, width: 100.0, height: 100.0)), - new Container(key: child2Key, width: 100.0, height: 100.0), - ] - ) + new Container(key: child0Key, width: 100.0, height: 100.0, child: log(1)), + new Expanded(child: new Container(key: child1Key, width: 100.0, height: 100.0, child: log(2))), + new Container(key: child2Key, width: 100.0, height: 100.0, child: log(3)), + ], + ), )); RenderBox renderBox; @@ -51,9 +356,12 @@ void main() { expect(renderBox.size.height, equals(100.0)); boxParentData = renderBox.parentData; expect(boxParentData.offset.dx, equals(700.0)); + + expect(OrderPainter.log, [1, 2, 3]); }); - testWidgets('Row with default main axis parameters', (WidgetTester tester) async { + testWidgets('Row with default main axis parameters - LTR', (WidgetTester tester) async { + OrderPainter.log.clear(); final Key rowKey = const Key('row'); final Key child0Key = const Key('child0'); final Key child1Key = const Key('child1'); @@ -65,12 +373,13 @@ void main() { await tester.pumpWidget(new Center( child: new Row( key: rowKey, + textDirection: TextDirection.ltr, children: [ - new Container(key: child0Key, width: 100.0, height: 100.0), - new Container(key: child1Key, width: 100.0, height: 100.0), - new Container(key: child2Key, width: 100.0, height: 100.0), - ] - ) + new Container(key: child0Key, width: 100.0, height: 100.0, child: log(1)), + new Container(key: child1Key, width: 100.0, height: 100.0, child: log(2)), + new Container(key: child2Key, width: 100.0, height: 100.0, child: log(3)), + ], + ), )); RenderBox renderBox; @@ -97,9 +406,12 @@ void main() { expect(renderBox.size.height, equals(100.0)); boxParentData = renderBox.parentData; expect(boxParentData.offset.dx, equals(200.0)); + + expect(OrderPainter.log, [1, 2, 3]); }); - testWidgets('Row with MainAxisAlignment.center', (WidgetTester tester) async { + testWidgets('Row with MainAxisAlignment.center - LTR', (WidgetTester tester) async { + OrderPainter.log.clear(); final Key rowKey = const Key('row'); final Key child0Key = const Key('child0'); final Key child1Key = const Key('child1'); @@ -110,11 +422,12 @@ void main() { child: new Row( key: rowKey, mainAxisAlignment: MainAxisAlignment.center, + textDirection: TextDirection.ltr, children: [ - new Container(key: child0Key, width: 100.0, height: 100.0), - new Container(key: child1Key, width: 100.0, height: 100.0), - ] - ) + new Container(key: child0Key, width: 100.0, height: 100.0, child: log(1)), + new Container(key: child1Key, width: 100.0, height: 100.0, child: log(2)), + ], + ), )); RenderBox renderBox; @@ -135,9 +448,12 @@ void main() { expect(renderBox.size.height, equals(100.0)); boxParentData = renderBox.parentData; expect(boxParentData.offset.dx, equals(400.0)); + + expect(OrderPainter.log, [1, 2]); }); - testWidgets('Row with MainAxisAlignment.end', (WidgetTester tester) async { + testWidgets('Row with MainAxisAlignment.end - LTR', (WidgetTester tester) async { + OrderPainter.log.clear(); final Key rowKey = const Key('row'); final Key child0Key = const Key('child0'); final Key child1Key = const Key('child1'); @@ -148,13 +464,14 @@ void main() { await tester.pumpWidget(new Center( child: new Row( key: rowKey, + textDirection: TextDirection.ltr, mainAxisAlignment: MainAxisAlignment.end, children: [ - new Container(key: child0Key, width: 100.0, height: 100.0), - new Container(key: child1Key, width: 100.0, height: 100.0), - new Container(key: child2Key, width: 100.0, height: 100.0), - ] - ) + new Container(key: child0Key, width: 100.0, height: 100.0, child: log(1)), + new Container(key: child1Key, width: 100.0, height: 100.0, child: log(2)), + new Container(key: child2Key, width: 100.0, height: 100.0, child: log(3)), + ], + ), )); RenderBox renderBox; @@ -181,9 +498,12 @@ void main() { expect(renderBox.size.height, equals(100.0)); boxParentData = renderBox.parentData; expect(boxParentData.offset.dx, equals(700.0)); + + expect(OrderPainter.log, [1, 2, 3]); }); - testWidgets('Row with MainAxisAlignment.spaceBetween', (WidgetTester tester) async { + testWidgets('Row with MainAxisAlignment.spaceBetween - LTR', (WidgetTester tester) async { + OrderPainter.log.clear(); final Key rowKey = const Key('row'); final Key child0Key = const Key('child0'); final Key child1Key = const Key('child1'); @@ -195,12 +515,13 @@ void main() { child: new Row( key: rowKey, mainAxisAlignment: MainAxisAlignment.spaceBetween, + textDirection: TextDirection.ltr, children: [ - new Container(key: child0Key, width: 100.0, height: 100.0), - new Container(key: child1Key, width: 100.0, height: 100.0), - new Container(key: child2Key, width: 100.0, height: 100.0), - ] - ) + new Container(key: child0Key, width: 100.0, height: 100.0, child: log(1)), + new Container(key: child1Key, width: 100.0, height: 100.0, child: log(2)), + new Container(key: child2Key, width: 100.0, height: 100.0, child: log(3)), + ], + ), )); RenderBox renderBox; @@ -227,9 +548,12 @@ void main() { expect(renderBox.size.height, equals(100.0)); boxParentData = renderBox.parentData; expect(boxParentData.offset.dx, equals(700.0)); + + expect(OrderPainter.log, [1, 2, 3]); }); - testWidgets('Row with MainAxisAlignment.spaceAround', (WidgetTester tester) async { + testWidgets('Row with MainAxisAlignment.spaceAround - LTR', (WidgetTester tester) async { + OrderPainter.log.clear(); final Key rowKey = const Key('row'); final Key child0Key = const Key('child0'); final Key child1Key = const Key('child1'); @@ -242,13 +566,14 @@ void main() { child: new Row( key: rowKey, mainAxisAlignment: MainAxisAlignment.spaceAround, + textDirection: TextDirection.ltr, children: [ - new Container(key: child0Key, width: 100.0, height: 100.0), - new Container(key: child1Key, width: 100.0, height: 100.0), - new Container(key: child2Key, width: 100.0, height: 100.0), - new Container(key: child3Key, width: 100.0, height: 100.0), - ] - ) + new Container(key: child0Key, width: 100.0, height: 100.0, child: log(1)), + new Container(key: child1Key, width: 100.0, height: 100.0, child: log(2)), + new Container(key: child2Key, width: 100.0, height: 100.0, child: log(3)), + new Container(key: child3Key, width: 100.0, height: 100.0, child: log(4)), + ], + ), )); RenderBox renderBox; @@ -281,9 +606,12 @@ void main() { expect(renderBox.size.height, equals(100.0)); boxParentData = renderBox.parentData; expect(boxParentData.offset.dx, equals(650.0)); + + expect(OrderPainter.log, [1, 2, 3, 4]); }); - testWidgets('Row with MainAxisAlignment.spaceEvenly', (WidgetTester tester) async { + testWidgets('Row with MainAxisAlignment.spaceEvenly - LTR', (WidgetTester tester) async { + OrderPainter.log.clear(); final Key rowKey = const Key('row'); final Key child0Key = const Key('child0'); final Key child1Key = const Key('child1'); @@ -295,12 +623,13 @@ void main() { child: new Row( key: rowKey, mainAxisAlignment: MainAxisAlignment.spaceEvenly, + textDirection: TextDirection.ltr, children: [ - new Container(key: child0Key, width: 200.0, height: 100.0), - new Container(key: child1Key, width: 200.0, height: 100.0), - new Container(key: child2Key, width: 200.0, height: 100.0), - ] - ) + new Container(key: child0Key, width: 200.0, height: 100.0, child: log(1)), + new Container(key: child1Key, width: 200.0, height: 100.0, child: log(2)), + new Container(key: child2Key, width: 200.0, height: 100.0, child: log(3)), + ], + ), )); RenderBox renderBox; @@ -327,9 +656,12 @@ void main() { expect(renderBox.size.height, equals(100.0)); boxParentData = renderBox.parentData; expect(boxParentData.offset.dx, equals(550.0)); + + expect(OrderPainter.log, [1, 2, 3]); }); - testWidgets('Row and MainAxisSize.min', (WidgetTester tester) async { + testWidgets('Row and MainAxisSize.min - LTR', (WidgetTester tester) async { + OrderPainter.log.clear(); final Key rowKey = const Key('rowKey'); final Key child0Key = const Key('child0'); final Key child1Key = const Key('child1'); @@ -340,11 +672,12 @@ void main() { child: new Row( key: rowKey, mainAxisSize: MainAxisSize.min, + textDirection: TextDirection.ltr, children: [ - new Container(key: child0Key, width: 100.0, height: 100.0), - new Container(key: child1Key, width: 150.0, height: 100.0) - ] - ) + new Container(key: child0Key, width: 100.0, height: 100.0, child: log(1)), + new Container(key: child1Key, width: 150.0, height: 100.0, child: log(2)), + ], + ), )); RenderBox renderBox; @@ -365,26 +698,454 @@ void main() { expect(renderBox.size.height, equals(100.0)); boxParentData = renderBox.parentData; expect(boxParentData.offset.dx, equals(100.0)); + + expect(OrderPainter.log, [1, 2]); }); - testWidgets('Row MainAxisSize.min layout at zero size', (WidgetTester tester) async { + testWidgets('Row MainAxisSize.min layout at zero size - LTR', (WidgetTester tester) async { + OrderPainter.log.clear(); final Key childKey = const Key('childKey'); await tester.pumpWidget(new Center( child: new Container( width: 0.0, height: 0.0, - child: new Row( + child: new Row( + textDirection: TextDirection.ltr, + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, children: [ new Container( key: childKey, width: 100.0, - height: 100.0 - ) + height: 100.0, + ), ], - mainAxisSize: MainAxisSize.min - ) - ) + ), + ), + )); + + final RenderBox renderBox = tester.renderObject(find.byKey(childKey)); + expect(renderBox.size.width, equals(100.0)); + expect(renderBox.size.height, equals(0.0)); + }); + + + // RTL + + testWidgets('Row with one Flexible child - RTL', (WidgetTester tester) async { + OrderPainter.log.clear(); + final Key rowKey = const Key('row'); + final Key child0Key = const Key('child0'); + final Key child1Key = const Key('child1'); + final Key child2Key = const Key('child2'); + + // Default is MainAxisSize.max so the Row should be as wide as the test: 800. + // Default is MainAxisAlignment.start so children so the children's + // right edges should be at 0, 100, 700 from the right, child2's width should be 600 + await tester.pumpWidget(new Center( + child: new Row( + key: rowKey, + textDirection: TextDirection.rtl, + children: [ + new Container(key: child0Key, width: 100.0, height: 100.0, child: log(1)), + new Expanded(child: new Container(key: child1Key, width: 100.0, height: 100.0, child: log(2))), + new Container(key: child2Key, width: 100.0, height: 100.0, child: log(3)), + ], + ), + )); + + RenderBox renderBox; + BoxParentData boxParentData; + + renderBox = tester.renderObject(find.byKey(rowKey)); + expect(renderBox.size.width, equals(800.0)); + expect(renderBox.size.height, equals(100.0)); + + renderBox = tester.renderObject(find.byKey(child0Key)); + expect(renderBox.size.width, equals(100.0)); + expect(renderBox.size.height, equals(100.0)); + boxParentData = renderBox.parentData; + expect(boxParentData.offset.dx, equals(700.0)); + + renderBox = tester.renderObject(find.byKey(child1Key)); + expect(renderBox.size.width, equals(600.0)); + expect(renderBox.size.height, equals(100.0)); + boxParentData = renderBox.parentData; + expect(boxParentData.offset.dx, equals(100.0)); + + renderBox = tester.renderObject(find.byKey(child2Key)); + expect(renderBox.size.width, equals(100.0)); + expect(renderBox.size.height, equals(100.0)); + boxParentData = renderBox.parentData; + expect(boxParentData.offset.dx, equals(0.0)); + + expect(OrderPainter.log, [1, 2, 3]); + }); + + testWidgets('Row with default main axis parameters - RTL', (WidgetTester tester) async { + OrderPainter.log.clear(); + final Key rowKey = const Key('row'); + final Key child0Key = const Key('child0'); + final Key child1Key = const Key('child1'); + final Key child2Key = const Key('child2'); + + // Default is MainAxisSize.max so the Row should be as wide as the test: 800. + // Default is MainAxisAlignment.start so children so the children's + // right edges should be at 0, 100, 200 from the right + await tester.pumpWidget(new Center( + child: new Row( + key: rowKey, + textDirection: TextDirection.rtl, + children: [ + new Container(key: child0Key, width: 100.0, height: 100.0, child: log(1)), + new Container(key: child1Key, width: 100.0, height: 100.0, child: log(2)), + new Container(key: child2Key, width: 100.0, height: 100.0, child: log(3)), + ], + ), + )); + + RenderBox renderBox; + BoxParentData boxParentData; + + renderBox = tester.renderObject(find.byKey(rowKey)); + expect(renderBox.size.width, equals(800.0)); + expect(renderBox.size.height, equals(100.0)); + + renderBox = tester.renderObject(find.byKey(child0Key)); + expect(renderBox.size.width, equals(100.0)); + expect(renderBox.size.height, equals(100.0)); + boxParentData = renderBox.parentData; + expect(boxParentData.offset.dx, equals(700.0)); + + renderBox = tester.renderObject(find.byKey(child1Key)); + expect(renderBox.size.width, equals(100.0)); + expect(renderBox.size.height, equals(100.0)); + boxParentData = renderBox.parentData; + expect(boxParentData.offset.dx, equals(600.0)); + + renderBox = tester.renderObject(find.byKey(child2Key)); + expect(renderBox.size.width, equals(100.0)); + expect(renderBox.size.height, equals(100.0)); + boxParentData = renderBox.parentData; + expect(boxParentData.offset.dx, equals(500.0)); + + expect(OrderPainter.log, [1, 2, 3]); + }); + + testWidgets('Row with MainAxisAlignment.center - RTL', (WidgetTester tester) async { + OrderPainter.log.clear(); + final Key rowKey = const Key('row'); + final Key child0Key = const Key('child0'); + final Key child1Key = const Key('child1'); + + // Default is MainAxisSize.max so the Row should be as wide as the test: 800. + // The 100x100 children's right edges should be at 300, 400 from the right + await tester.pumpWidget(new Center( + child: new Row( + key: rowKey, + mainAxisAlignment: MainAxisAlignment.center, + textDirection: TextDirection.rtl, + children: [ + new Container(key: child0Key, width: 100.0, height: 100.0, child: log(1)), + new Container(key: child1Key, width: 100.0, height: 100.0, child: log(2)), + ], + ), + )); + + RenderBox renderBox; + BoxParentData boxParentData; + + renderBox = tester.renderObject(find.byKey(rowKey)); + expect(renderBox.size.width, equals(800.0)); + expect(renderBox.size.height, equals(100.0)); + + renderBox = tester.renderObject(find.byKey(child0Key)); + expect(renderBox.size.width, equals(100.0)); + expect(renderBox.size.height, equals(100.0)); + boxParentData = renderBox.parentData; + expect(boxParentData.offset.dx, equals(400.0)); + + renderBox = tester.renderObject(find.byKey(child1Key)); + expect(renderBox.size.width, equals(100.0)); + expect(renderBox.size.height, equals(100.0)); + boxParentData = renderBox.parentData; + expect(boxParentData.offset.dx, equals(300.0)); + + expect(OrderPainter.log, [1, 2]); + }); + + testWidgets('Row with MainAxisAlignment.end - RTL', (WidgetTester tester) async { + OrderPainter.log.clear(); + final Key rowKey = const Key('row'); + final Key child0Key = const Key('child0'); + final Key child1Key = const Key('child1'); + final Key child2Key = const Key('child2'); + + // Default is MainAxisSize.max so the Row should be as wide as the test: 800. + // The 100x100 children's right edges should be at 500, 600, 700 from the right. + await tester.pumpWidget(new Center( + child: new Row( + key: rowKey, + textDirection: TextDirection.rtl, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + new Container(key: child0Key, width: 100.0, height: 100.0, child: log(1)), + new Container(key: child1Key, width: 100.0, height: 100.0, child: log(2)), + new Container(key: child2Key, width: 100.0, height: 100.0, child: log(3)), + ], + ), + )); + + RenderBox renderBox; + BoxParentData boxParentData; + + renderBox = tester.renderObject(find.byKey(rowKey)); + expect(renderBox.size.width, equals(800.0)); + expect(renderBox.size.height, equals(100.0)); + + renderBox = tester.renderObject(find.byKey(child0Key)); + expect(renderBox.size.width, equals(100.0)); + expect(renderBox.size.height, equals(100.0)); + boxParentData = renderBox.parentData; + expect(boxParentData.offset.dx, equals(200.0)); + + renderBox = tester.renderObject(find.byKey(child1Key)); + expect(renderBox.size.width, equals(100.0)); + expect(renderBox.size.height, equals(100.0)); + boxParentData = renderBox.parentData; + expect(boxParentData.offset.dx, equals(100.0)); + + renderBox = tester.renderObject(find.byKey(child2Key)); + expect(renderBox.size.width, equals(100.0)); + expect(renderBox.size.height, equals(100.0)); + boxParentData = renderBox.parentData; + expect(boxParentData.offset.dx, equals(0.0)); + + expect(OrderPainter.log, [1, 2, 3]); + }); + + testWidgets('Row with MainAxisAlignment.spaceBetween - RTL', (WidgetTester tester) async { + OrderPainter.log.clear(); + final Key rowKey = const Key('row'); + final Key child0Key = const Key('child0'); + final Key child1Key = const Key('child1'); + final Key child2Key = const Key('child2'); + + // Default is MainAxisSize.max so the Row should be as wide as the test: 800. + // The 100x100 children's right edges should be at 0, 350, 700 from the right + await tester.pumpWidget(new Center( + child: new Row( + key: rowKey, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + textDirection: TextDirection.rtl, + children: [ + new Container(key: child0Key, width: 100.0, height: 100.0, child: log(1)), + new Container(key: child1Key, width: 100.0, height: 100.0, child: log(2)), + new Container(key: child2Key, width: 100.0, height: 100.0, child: log(3)), + ], + ), + )); + + RenderBox renderBox; + BoxParentData boxParentData; + + renderBox = tester.renderObject(find.byKey(rowKey)); + expect(renderBox.size.width, equals(800.0)); + expect(renderBox.size.height, equals(100.0)); + + renderBox = tester.renderObject(find.byKey(child0Key)); + expect(renderBox.size.width, equals(100.0)); + expect(renderBox.size.height, equals(100.0)); + boxParentData = renderBox.parentData; + expect(boxParentData.offset.dx, equals(700.0)); + + renderBox = tester.renderObject(find.byKey(child1Key)); + expect(renderBox.size.width, equals(100.0)); + expect(renderBox.size.height, equals(100.0)); + boxParentData = renderBox.parentData; + expect(boxParentData.offset.dx, equals(350.0)); + + renderBox = tester.renderObject(find.byKey(child2Key)); + expect(renderBox.size.width, equals(100.0)); + expect(renderBox.size.height, equals(100.0)); + boxParentData = renderBox.parentData; + expect(boxParentData.offset.dx, equals(0.0)); + + expect(OrderPainter.log, [1, 2, 3]); + }); + + testWidgets('Row with MainAxisAlignment.spaceAround - RTL', (WidgetTester tester) async { + OrderPainter.log.clear(); + final Key rowKey = const Key('row'); + final Key child0Key = const Key('child0'); + final Key child1Key = const Key('child1'); + final Key child2Key = const Key('child2'); + final Key child3Key = const Key('child3'); + + // Default is MainAxisSize.max so the Row should be as wide as the test: 800. + // The 100x100 children's right edges should be at 50, 250, 450, 650 from the right + await tester.pumpWidget(new Center( + child: new Row( + key: rowKey, + mainAxisAlignment: MainAxisAlignment.spaceAround, + textDirection: TextDirection.rtl, + children: [ + new Container(key: child0Key, width: 100.0, height: 100.0, child: log(1)), + new Container(key: child1Key, width: 100.0, height: 100.0, child: log(2)), + new Container(key: child2Key, width: 100.0, height: 100.0, child: log(3)), + new Container(key: child3Key, width: 100.0, height: 100.0, child: log(4)), + ], + ), + )); + + RenderBox renderBox; + BoxParentData boxParentData; + + renderBox = tester.renderObject(find.byKey(rowKey)); + expect(renderBox.size.width, equals(800.0)); + expect(renderBox.size.height, equals(100.0)); + + renderBox = tester.renderObject(find.byKey(child0Key)); + expect(renderBox.size.width, equals(100.0)); + expect(renderBox.size.height, equals(100.0)); + boxParentData = renderBox.parentData; + expect(boxParentData.offset.dx, equals(650.0)); + + renderBox = tester.renderObject(find.byKey(child1Key)); + expect(renderBox.size.width, equals(100.0)); + expect(renderBox.size.height, equals(100.0)); + boxParentData = renderBox.parentData; + expect(boxParentData.offset.dx, equals(450.0)); + + renderBox = tester.renderObject(find.byKey(child2Key)); + expect(renderBox.size.width, equals(100.0)); + expect(renderBox.size.height, equals(100.0)); + boxParentData = renderBox.parentData; + expect(boxParentData.offset.dx, equals(250.0)); + + renderBox = tester.renderObject(find.byKey(child3Key)); + expect(renderBox.size.width, equals(100.0)); + expect(renderBox.size.height, equals(100.0)); + boxParentData = renderBox.parentData; + expect(boxParentData.offset.dx, equals(50.0)); + + expect(OrderPainter.log, [1, 2, 3, 4]); + }); + + testWidgets('Row with MainAxisAlignment.spaceEvenly - RTL', (WidgetTester tester) async { + OrderPainter.log.clear(); + final Key rowKey = const Key('row'); + final Key child0Key = const Key('child0'); + final Key child1Key = const Key('child1'); + final Key child2Key = const Key('child2'); + + // Default is MainAxisSize.max so the Row should be as wide as the test: 800. + // The 200x100 children's right edges should be at 50, 300, 550 from the right + await tester.pumpWidget(new Center( + child: new Row( + key: rowKey, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + textDirection: TextDirection.rtl, + children: [ + new Container(key: child0Key, width: 200.0, height: 100.0, child: log(1)), + new Container(key: child1Key, width: 200.0, height: 100.0, child: log(2)), + new Container(key: child2Key, width: 200.0, height: 100.0, child: log(3)), + ], + ), + )); + + RenderBox renderBox; + BoxParentData boxParentData; + + renderBox = tester.renderObject(find.byKey(rowKey)); + expect(renderBox.size.width, equals(800.0)); + expect(renderBox.size.height, equals(100.0)); + + renderBox = tester.renderObject(find.byKey(child0Key)); + expect(renderBox.size.width, equals(200.0)); + expect(renderBox.size.height, equals(100.0)); + boxParentData = renderBox.parentData; + expect(boxParentData.offset.dx, equals(550.0)); + + renderBox = tester.renderObject(find.byKey(child1Key)); + expect(renderBox.size.width, equals(200.0)); + expect(renderBox.size.height, equals(100.0)); + boxParentData = renderBox.parentData; + expect(boxParentData.offset.dx, equals(300.0)); + + renderBox = tester.renderObject(find.byKey(child2Key)); + expect(renderBox.size.width, equals(200.0)); + expect(renderBox.size.height, equals(100.0)); + boxParentData = renderBox.parentData; + expect(boxParentData.offset.dx, equals(50.0)); + + expect(OrderPainter.log, [1, 2, 3]); + }); + + testWidgets('Row and MainAxisSize.min - RTL', (WidgetTester tester) async { + OrderPainter.log.clear(); + final Key rowKey = const Key('rowKey'); + final Key child0Key = const Key('child0'); + final Key child1Key = const Key('child1'); + + // Row with MainAxisSize.min without flexible children shrink wraps. + // Row's width should be 250, children should be at 0, 100 from right. + await tester.pumpWidget(new Center( + child: new Row( + key: rowKey, + mainAxisSize: MainAxisSize.min, + textDirection: TextDirection.rtl, + children: [ + new Container(key: child0Key, width: 100.0, height: 100.0, child: log(1)), + new Container(key: child1Key, width: 150.0, height: 100.0, child: log(2)), + ], + ), + )); + + RenderBox renderBox; + BoxParentData boxParentData; + + renderBox = tester.renderObject(find.byKey(rowKey)); + expect(renderBox.size.width, equals(250.0)); + expect(renderBox.size.height, equals(100.0)); + + renderBox = tester.renderObject(find.byKey(child0Key)); + expect(renderBox.size.width, equals(100.0)); + expect(renderBox.size.height, equals(100.0)); + boxParentData = renderBox.parentData; + expect(boxParentData.offset.dx, equals(150.0)); + + renderBox = tester.renderObject(find.byKey(child1Key)); + expect(renderBox.size.width, equals(150.0)); + expect(renderBox.size.height, equals(100.0)); + boxParentData = renderBox.parentData; + expect(boxParentData.offset.dx, equals(0.0)); + + expect(OrderPainter.log, [1, 2]); + }); + + testWidgets('Row MainAxisSize.min layout at zero size - RTL', (WidgetTester tester) async { + OrderPainter.log.clear(); + final Key childKey = const Key('childKey'); + + await tester.pumpWidget(new Center( + child: new Container( + width: 0.0, + height: 0.0, + child: new Row( + textDirection: TextDirection.rtl, + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + new Container( + key: childKey, + width: 100.0, + height: 100.0, + ), + ], + ), + ), )); final RenderBox renderBox = tester.renderObject(find.byKey(childKey)); diff --git a/packages/flutter/test/widgets/rtl_test.dart b/packages/flutter/test/widgets/rtl_test.dart new file mode 100644 index 0000000000..2ab136d64e --- /dev/null +++ b/packages/flutter/test/widgets/rtl_test.dart @@ -0,0 +1,70 @@ +// Copyright 2016 The Chromium 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_test/flutter_test.dart'; +import 'package:flutter/widgets.dart'; + +void main() { + testWidgets('Padding RTL', (WidgetTester tester) async { + final Widget child = new Padding( + padding: new EdgeInsetsDirectional.only(start: 10.0), + child: new Placeholder(), + ); + await tester.pumpWidget(new Directionality( + textDirection: TextDirection.ltr, + child: child, + )); + expect(tester.getTopLeft(find.byType(Placeholder)), const Offset(10.0, 0.0)); + await tester.pumpWidget(new Directionality( + textDirection: TextDirection.rtl, + child: child, + )); + expect(tester.getTopLeft(find.byType(Placeholder)), const Offset(0.0, 0.0)); + }); + + testWidgets('Container padding/margin RTL', (WidgetTester tester) async { + final Widget child = new Container( + padding: new EdgeInsetsDirectional.only(start: 6.0), + margin: new EdgeInsetsDirectional.only(end: 20.0, start: 4.0), + child: new Placeholder(), + ); + await tester.pumpWidget(new Directionality( + textDirection: TextDirection.ltr, + child: child, + )); + expect(tester.getTopLeft(find.byType(Placeholder)), const Offset(10.0, 0.0)); + expect(tester.getTopRight(find.byType(Placeholder)), const Offset(780.0, 0.0)); + await tester.pumpWidget(new Directionality( + textDirection: TextDirection.rtl, + child: child, + )); + expect(tester.getTopLeft(find.byType(Placeholder)), const Offset(20.0, 0.0)); + expect(tester.getTopRight(find.byType(Placeholder)), const Offset(790.0, 0.0)); + }); + + testWidgets('Container padding/margin mixed RTL/absolute', (WidgetTester tester) async { + final Widget child = new Container( + padding: new EdgeInsets.only(left: 6.0), + margin: new EdgeInsetsDirectional.only(end: 20.0, start: 4.0), + child: new Placeholder(), + ); + await tester.pumpWidget(new Directionality( + textDirection: TextDirection.ltr, + child: child, + )); + expect(tester.getTopLeft(find.byType(Placeholder)), const Offset(10.0, 0.0)); + expect(tester.getTopRight(find.byType(Placeholder)), const Offset(780.0, 0.0)); + await tester.pumpWidget(new Directionality( + textDirection: TextDirection.rtl, + child: child, + )); + expect(tester.getTopLeft(find.byType(Placeholder)), const Offset(26.0, 0.0)); + expect(tester.getTopRight(find.byType(Placeholder)), const Offset(796.0, 0.0)); + }); + + testWidgets('EdgeInsetsDirectional without Directionality', (WidgetTester tester) async { + await tester.pumpWidget(new Padding(padding: new EdgeInsetsDirectional.only())); + expect(tester.takeException(), isAssertionError); + }); +} diff --git a/packages/flutter_test/lib/src/binding.dart b/packages/flutter_test/lib/src/binding.dart index 70042adc1b..094c7474d2 100644 --- a/packages/flutter_test/lib/src/binding.dart +++ b/packages/flutter_test/lib/src/binding.dart @@ -433,7 +433,7 @@ abstract class TestWidgetsFlutterBinding extends BindingBase } )); assert(_parentZone != null); - assert(_pendingExceptionDetails != null); + assert(_pendingExceptionDetails != null, 'A test overrode FlutterError.onError but either failed to return it to its original state, or had unexpected additional errors that it could not handle. Typically, this is caused by using expect() before restoring FlutterError.onError.'); _parentZone.run(_testCompletionHandler); } ); diff --git a/packages/flutter_test/test/widget_tester_test.dart b/packages/flutter_test/test/widget_tester_test.dart index db5a57818a..e900de869c 100644 --- a/packages/flutter_test/test/widget_tester_test.dart +++ b/packages/flutter_test/test/widget_tester_test.dart @@ -140,33 +140,42 @@ void main() { group('find.descendant', () { testWidgets('finds one descendant', (WidgetTester tester) async { - await tester.pumpWidget(new Row(children: [ - new Column(children: [const Text('foo'), const Text('bar')]) - ])); + await tester.pumpWidget(new Row( + textDirection: TextDirection.ltr, + children: [ + new Column(children: [const Text('foo'), const Text('bar')]), + ], + )); expect(find.descendant( of: find.widgetWithText(Row, 'foo'), - matching: find.text('bar') + matching: find.text('bar'), ), findsOneWidget); }); testWidgets('finds two descendants with different ancestors', (WidgetTester tester) async { - await tester.pumpWidget(new Row(children: [ - new Column(children: [const Text('foo'), const Text('bar')]), - new Column(children: [const Text('foo'), const Text('bar')]), - ])); + await tester.pumpWidget(new Row( + textDirection: TextDirection.ltr, + children: [ + new Column(children: [const Text('foo'), const Text('bar')]), + new Column(children: [const Text('foo'), const Text('bar')]), + ], + )); expect(find.descendant( of: find.widgetWithText(Column, 'foo'), - matching: find.text('bar') + matching: find.text('bar'), ), findsNWidgets(2)); }); testWidgets('fails with a descriptive message', (WidgetTester tester) async { - await tester.pumpWidget(new Row(children: [ - new Column(children: [const Text('foo')]), - const Text('bar') - ])); + await tester.pumpWidget(new Row( + textDirection: TextDirection.ltr, + children: [ + new Column(children: [const Text('foo')]), + const Text('bar'), + ], + )); TestFailure failure; try { @@ -186,9 +195,12 @@ void main() { }); testWidgets('Root not matched by default', (WidgetTester tester) async { - await tester.pumpWidget(new Row(children: [ - new Column(children: [const Text('foo'), const Text('bar')]) - ])); + await tester.pumpWidget(new Row( + textDirection: TextDirection.ltr, + children: [ + new Column(children: [const Text('foo'), const Text('bar')]), + ], + )); expect(find.descendant( of: find.widgetWithText(Row, 'foo'), @@ -197,9 +209,12 @@ void main() { }); testWidgets('Match the root', (WidgetTester tester) async { - await tester.pumpWidget(new Row(children: [ - new Column(children: [const Text('foo'), const Text('bar')]) - ])); + await tester.pumpWidget(new Row( + textDirection: TextDirection.ltr, + children: [ + new Column(children: [const Text('foo'), const Text('bar')]), + ], + )); expect(find.descendant( of: find.widgetWithText(Row, 'foo'),